Canadian Locomotive Logistics

Controlling Room Lights – with JS

Building a Web-Based Smart-Switch Controller with Node.js, LIFX and Tuya

At Canadian Locomotive Logistics, we have been developing browser-based graphics user interfaces that allow visitors to operate model locomotives remotely. As these interfaces became more capable, we began looking beyond locomotive control. We wanted the layout room itself to respond when a remote operator opened or closed a control page.

That led to a new project: a small Node.js server that could control both LIFX and Tuya smart products through simple web addresses.

The result uses two main files:

– `server.js`, which receives web requests and sends commands to the appropriate smart-device service.
– `devices.json`, which contains the settings for each controllable light or switch.

The system currently allows our Layout 1 and Layout 2 web interfaces to turn room lights and smart switches on or off. It can also delay an ON command and automatically turn a device off after a preset amount of time.

Why We Built Our Own Controller

LIFX and Tuya products can already be controlled from their mobile apps, but our goal was different. We wanted the controls to be part of the locomotive operating experience.

For example, when somebody opens the Layout 2 graphics user interface, the interface can request:

        https://lighting.canadianlocomotivelogistics.ca/layout#/light#/on

When that person finishes operating, the interface can request the corresponding OFF address:

        https://lighting.canadianlocomotivelogistics.ca/layout#/light#/off

The operator does not need the Tuya or LIFX app, and the browser never receives our private cloud credentials. The browser sends a simple command to our server, and the server handles the secure communication with the appropriate service.

The Overall Design

Our Node.js application runs on a Synology DS1821+ NAS. It listens internally on port 3000. A Synology reverse proxy gives the service a public HTTPS address, so our browser-based graphics user interfaces can reach it securely.

A –> B [“Locomotive graphics interface”]
B –> C [“HTTPS reverse proxy on Synology NAS”]
C –> D [“Node.js server on port 3000”]
D –> E {“Device type in devices.json”}
E –>|LIFX| F[“LIFX cloud service”]
E –>|Tuya| G[“Tuya cloud service”]
F –> H[“Layout 1 light”]
G –> I[“Layout 2 smart switch”]

This arrangement gives us one consistent set of web commands even though the devices come from different manufacturers.

What `server.js` Does

The `server.js` file is the working part of the system. Its main responsibilities are:

1. Start a web server and listen for incoming requests.
2. Read the device definitions from `devices.json`.
3. Match the requested layout, device and action to the correct configuration entry.
4. Determine whether the selected device is a LIFX or Tuya product.
5. Send the command through the correct manufacturer’s cloud API.
6. Apply any configured ON delay or automatic-OFF timer.
7. Return a success or error response to the graphics user interface.

Instead of writing separate application code for every light, the server uses a general route pattern:

     /{layout}/{device}/{action}

That gives us addresses such as:

         /layout1/light1/on
         /layout1/light1/off
         /layout2/light1/on
         /layout2/light1/off

Adding another device does not require creating a completely different server. In most cases, we add another entry to `devices.json` and allow the existing route-handling code to do the rest.

What `devices.json` Does

The `devices.json` file separates device information from the main program. That makes the system easier to maintain and expand.

A simplified entry can look like this:

         {
         “name”: “layout2/light1”,
         “type”: “tuya”,
        “deviceId”:”TUYA_DEVICE_ID”,
         “timer”: 1800,
        “delayOnSeconds”: 0
         }

The important fields are:

        | Field | Purpose |
        | `name` | The layout and device name used in the web address. |
        | `type` | Tells the server whether to use the `tuya` or `lifx` control method. |
        | `deviceId` | Identifies the physical light or smart switch. |
        | `timer` | Number of seconds before the server automatically turns the device off. A value of 1800 equals 30 minutes. |
        | `delayOnSeconds` | Number of seconds the server waits before sending the ON command. |

The real device identifiers and API credentials should be treated as private information. Credentials should not be placed in browser JavaScript or published with a blog article. They belong on the server, preferably in protected environment variables or another private configuration source.

How an ON Request Is Processed

When a user opens one of our graphics interfaces, the page uses JavaScript `fetch()` to call the appropriate ON address. The server then follows a predictable sequence.

        sequenceDiagram
        participant GUI as Graphics interface
        participant API as server.js
        participant CFG as devices.json
        participant Cloud as LIFX or Tuya cloud
        participant Switch as Smart device

        GUI->>API: Request /layout2/light1/on
        API->>CFG: Find layout2/light1
        CFG–>>API: Type, ID, delay and timer
        API->>API: Wait for ON delay if configured
        API->>Cloud: Send authenticated ON command
        Cloud->>Switch: Turn device on
        API->>API: Start automatic-OFF timer
        API–>>GUI: Return result
“`

If the device has an automatic-OFF timer, the server schedules an OFF command after the configured number of seconds. This is particularly useful for a public operating interface. If somebody leaves a page open, loses their connection or forgets to close it properly, the room lighting does not remain on indefinitely.

Delayed ON and Automatic OFF

Two of the most useful additions were the delayed-ON setting and the automatic-OFF timer.

The automatic-OFF timer was our first safeguard. For example, a timer value of 1800 seconds keeps the device on for a maximum of 30 minutes. Each device can have its own timer value in `devices.json`.

We later added `delayOnSeconds`. This allows the server to receive an ON request immediately but wait a specified number of seconds before energizing the device. Keeping this setting in `devices.json` lets us adjust the timing without rewriting the main server logic.

These settings also make the server useful for more than lighting. The same structure could eventually control scenery, signs, ventilation or other accessories, provided that switching them remotely is safe.

Connecting the Graphics Interface

The browser side of the system is intentionally simple. A page can turn on a device when the interface opens:

        fetch(“https://lighting.canadianlocomotivelogistics.ca/layout2/light1/on”)
        .then(response => response.json())
        .then(data => console.log(“Layout 2 light ON:”, data))
        .catch(error => console.error(“Light control error:”, error));

It can also make a best-effort request when the operator leaves the page:

        window.addEventListener(“pagehide”, function () {
        fetch(“https://lighting.canadianlocomotivelogistics.ca/layout2/light1/off”, {method: “GET”,keepalive: true}).catch(()=> {});
        });

The phrase “best effort” is important. Browsers do not guarantee that every request will finish while a page is closing, especially if the computer shuts down or the network connection disappears. That is why the server-side automatic-OFF timer remains an essential backup rather than relying entirely on the page-close event.

Supporting Both LIFX and Tuya

LIFX and Tuya do not use the same cloud-control method. The server therefore treats them as separate device types behind one common interface.

For a Tuya device, the server uses the device’s Tuya identifier and the credentials associated with our Tuya cloud project. It signs and sends the required request to the Tuya cloud service.

For a LIFX device, the server uses the LIFX cloud API and the device selector associated with the light. The LIFX authorization token remains on the server.

From the graphics interface’s point of view, these differences disappear. It only needs to know the web address for the requested device. `server.js` and `devices.json` decide which system must handle the command.

Why We Used a Reverse Proxy

The Node.js application listens on port 3000 inside our local network, but we did not want users connecting directly to the NAS by its local address or by an exposed application port.

The Synology reverse proxy provides several advantages:

        – It gives the service a normal HTTPS domain name.
        – It provides encrypted communication between the browser and our server.
        – It forwards public requests to the Node.js application running internally.
        – It keeps the internal NAS address and port out of the public graphics-interface code.
        – It allows the smart-device service to be managed alongside our other web services.

The public route and the internal service therefore remain separate:


        A[“Public HTTPS address”] –> B[“Synology reverse proxy”]
        B –> C[“127.0.0.1:3000”]
        C –> D[“server.js”]

Reliability and Security Lessons

This project taught us several practical lessons.

First, cloud credentials must stay on the server. Putting a Tuya secret or LIFX token in the graphics interface would allow anyone viewing the page source to copy it.

Second, every request should produce a useful response and be logged. When a device does not react, we need to know whether the route was incorrect, the configuration was missing, the cloud service rejected the request or the physical device was offline.

Third, public control routes should be protected against abuse. Depending on how widely the interface is distributed, useful protections can include rate limiting, permitted-origin checks, authentication, request logging and restrictions on which devices and actions are available.

Fourth, an OFF command should always have a backup. The page-close request is convenient, but the server-side timer is what protects us when a browser closes unexpectedly.

Finally, names should remain consistent. A clear name such as `layout2/light1` is much easier to understand and maintain than scattering manufacturer-specific device identifiers throughout several web pages.

Expanding the System

The most valuable part of this design is that it can grow without changing how operators use it. Additional lights can be added to `devices.json`, each with its own type, identifier, ON delay and automatic-OFF period.

Possible future additions include:

        – A protected status page showing whether each device is on, off or unavailable.
        – A manual control dashboard for the layout owner.
        – More detailed logging of commands and cloud responses.
        – Rate limiting and access tokens for public routes.
        – A cancellation system for delayed ON commands.
        – Automatic recovery of active timers after the Node.js service restarts.
        – Integration with JMRI so a locomotive function, turnout or sensor can trigger room lighting or layout accessories.

Conclusion

What began as a way to turn on one smart switch has become a flexible bridge between our web-based locomotive controls and the physical layout room.

By combining `server.js`, `devices.json`, the Synology reverse proxy and the LIFX and Tuya cloud services, we created one straightforward control system for products from different manufacturers. The graphics interface sends a simple web request, while the server handles device selection, authentication, delays and automatic shutdown.

For Canadian Locomotive Logistics, this is another step toward making remote model-railroad operation feel less like watching a video and more like interacting with a real layout. The locomotive, cameras, controls and now even the room lighting can work together as parts of the same online operating experience.

*Technical note: The examples in this article are simplified for publication. Private credentials, actual device identifiers and other security-sensitive values have deliberately been omitted

A sample copy of the server.js file can be found here https://canadianlocomotivelogistics.ca/wp-content/uploads/2026/08/server.js.txt

A sample copy of devices.json file can be found here http://canadianlocomotivelogistics.ca/wp-content/uploads/2026/08/devices.json

About the Author

Leave a Reply

Your email address will not be published. Required fields are marked *

You may also like these

No Related Post