vibescoder

Homelab Router Part 2: Lights Out for Hermes, All Locally

·8 min read

The local router became real when the lights went out.

Not metaphorically. Not in a benchmark. Not in a JSON response that looked plausible while the room stayed bright. The actual TV lights turned off after Hermes received a normal instruction, reached through the local router path, engaged an entirely local AI model, used Home Assistant tools, and sent the command through Z-Wave.

That is the difference between an architecture diagram and the start of a local smart home.

Part 1 proved the router could sit between Hermes and the local model fleet. It could keep Spark DeepSeek and 5090 Granite behind one OpenAI-compatible endpoint. It could hide unavailable models. It could make the homelab feel less like a pile of ports.

Part 2 had a stricter test. Could it turn off a light?

The Dongle Showed Up Before Home Assistant Could Use It

The new hardware was a SONOFF Z-Wave USB dongle on the ThinkCentre, the same always-on Proxmox box that hosts Home Assistant and the router. The host saw it immediately as a Silicon Labs CP210x serial device.

That was the easy part.

The first useful wrinkle was that I already had another SONOFF stick attached. Both adapters reported the same Silicon Labs USB vendor and product IDs. Passing through 10c4:ea60 was no longer specific enough because it matched both sticks.

So the Proxmox config had to pin each adapter by physical USB port instead of generic vendor/product identity:

usb0: existing SONOFF MG24 stick
usb1: new SONOFF Z-Wave stick

These are not real serial numbers or bus paths, obviously. The syntax is correct, though. When two USB radios share the same chip family, pass through the stable port or by-id path, not just the vendor/product pair.

Inside Home Assistant OS, both serial devices appeared under /dev/serial/by-id/. That gave the Z-Wave stack a durable path to use even if Linux renamed ttyUSB0 and ttyUSB1 on reboot.

Z-Wave JS Needed the Exact Device Path

I installed the official Z-Wave JS add-on and pointed it at the SONOFF Z-Wave dongle’s by-id path.

The first start failed with a blunt message:

Either 'device' or 'socket' must be configured

That was accurate. The add-on had installed. It had generated security keys. It had not yet been told which serial device to use.

Configuring the add-on through the Supervisor API fixed that. Once the device path was set, the logs changed from startup failure to controller bring-up:

supported Z-Wave features: SmartStart
Changed RF region to USA (Long Range)
received controller IDs
own node ID: 1
Promotion to SUC/SIS succeeded
[Node 001] The node is ready to be used
All nodes are ready to be used

Then I added the Home Assistant Z-Wave JS integration. Home Assistant discovered the controller as a Silicon Labs 700/800 Series adapter. The controller was alive. The network was empty. That is exactly where I wanted to be before touching the switches.

The Switches Joined Cleanly After SmartThings Let Go

The two wall switches were GE/Jasco dimmers that had been paired with SmartThings. The first step was not Home Assistant. It was divorce.

I excluded both switches from SmartThings using the physical paddle sequence. That mattered. Z-Wave devices generally belong to one network at a time. Trying to include a still-owned device into a new controller is how you waste an afternoon and convince yourself radio is cursed.

Once SmartThings let them go, I started inclusion from the Z-Wave JS controller and watched the events over the zwave-js-server Python client.

Switch one became node 2:

node found: Multilevel Switch
node added: Node 002
final nodes: [1, 2]

Switch two became node 3:

node found: Multilevel Switch
node added: Node 003
final nodes: [1, 2, 3]

Home Assistant identified both as GE/Jasco dimmers:

GE / Jasco In-Wall Paddle Dimmer, 300S
model: 12724 / ZW3005
firmware: 3.37

That was a small surprise. I grabbed the ZW4005 model from an uninstalled smart switch. Ends up that was a rocker switch and not a dimmer switch, which matters. The Z-Wave fingerprint said 12724 / ZW3005. I trust the device fingerprint more than my proxy check on an orphaned device.

The First Test Failed Because the Labels Were Backward

Home Assistant exposed the two dimmers as light.* entities, not switch.* entities. That is correct. They support brightness.

I named the first one Hallway Lights and the second one TV Lights. Then I turned off Hallway Lights.

Home Assistant said success.

The physical light did not change.

That is the kind of failure that makes local home automation maddening. The API call returned 200. The entity changed state. The dashboard looked convinced. The room disagreed.

I started down the technical rabbit hole. Were the Z-Wave values stale? Were the nodes optimistic? Was the Multilevel Switch command not reaching the load? Did the old GE dimmers need associations or a re-interview?

Then the simple answer arrived. The physical sequence was reversed.

Node 2 was TV Lights. Node 3 was Hallway Lights.

So I swapped the device labels and entity IDs:

Node 2 -> light.tv_lights
Node 3 -> light.hallway_lights

Then I turned on TV Lights. Indeed the home let there be light.

That is not a glamorous bug. It is the most common smart-home bug there is. The software is right, the labels are wrong, and the only reliable test is to look at the room.

Hermes Failed Once for the Right Reason

With Home Assistant working, I tried the real test:

Turn off TV Lights.

Hermes refused.

That was not a model failure. It was an integration failure. Hermes only had the fitness-tracker MCP configured. It did not have Home Assistant tools available in that session, so it did the right thing and said it could not control lights rather than faking the result.

That is an underrated success mode. A model that refuses because the tool is missing is better than a model that invents a home automation API and declares victory.

The fix was to add the Home Assistant MCP server to Hermes. There was one more stale-infrastructure wrinkle: the saved Home Assistant MCP URL pointed at the old Home Assistant address from an earlier network layout. Updating it to the current HA endpoint made the test pass.

Hermes then discovered 77 Home Assistant tools, including the ones that matter for this flow:

ha_get_state
ha_search
ha_call_service
ha_bulk_control

Now the stack had the missing bridge:

Hermes
  -> local-agent-router
  -> Grante 4.2 on AI Workstation
  -> Home Assistant MCP tools
  -> Z-Wave JS
  -> GE/Jasco dimmer

The Real Test Was One Sentence

The final prompt was intentionally boring:

Turn off TV Lights in Home Assistant. Use the Home Assistant tools. The entity is light.tv_lights.

Hermes answered:

Done. The TV Lights (light.tv_lights) in Home Assistant have been turned off.

Then I verified the state directly from Home Assistant:

light.tv_lights       off
light.hallway_lights  off

I confirmed the physical light was off.

That is the loop I wanted:

human language
  -> Hermes
  -> local router path
  -> local model/tool execution
  -> Home Assistant
  -> Z-Wave radio
  -> physical light state

This does not prove the whole system is production-ready. The prompt still included the exact entity name. Hermes’ default provider is not flipped to the router yet. The router has not implemented dynamic node heartbeat. I still need to test the natural-language version without telling Hermes the entity ID.

But this was the first time the router project did something that mattered in the physical house.


Part 1 made the fleet feel like one endpoint. Part 2 made that endpoint turn off a real light.

The next step is to remove more scaffolding. I want Hermes to hear “turn off the TV lights,” search Home Assistant, choose the right entity, route the request to the right local model, and complete the action without me giving it the entity ID.

After that, the router needs its next architectural step: machines should advertise their own capabilities instead of living in static YAML forever.

By the Numbers

  • 1 Z-Wave USB dongle added to the ThinkCentre/Home Assistant stack
  • 2 SONOFF USB radios now pass through to HAOS by physical USB port
  • 1 Z-Wave JS add-on installed and configured with a stable by-id serial path
  • 2 GE/Jasco dimmers excluded from SmartThings and included in Home Assistant
  • 3 Z-Wave nodes now exist: the controller plus two dimmers
  • 77 Home Assistant MCP tools discovered by Hermes after fixing the stale endpoint
  • 1 mislabeled device mapping caught by checking the physical room instead of trusting the API
  • 1 real light turned off through Hermes, Home Assistant, and Z-Wave, all locally

Comments