Skip to content

Multi-gateway & HA entities — state of play and phased plan

Filed: 2026-09-14 · Scope: franklinwh-direct-connect-bridge only.


1. What is already built

Multi-gateway is ~70% done and working, not a greenfield feature:

layer state detail
Config ✅ FWH_HOSTS — CSV of host or host=Label (10.0.0.5=Home,10.0.0.6=Shed), wins over FWH_HOST
Registry ✅ state.GatewayState, gateways(), get_gateway(id)
Poller ✅ one run_gateway task per gateway, each with its own caches and connectivity
MQTT / HA ✅ per-node topics prefix/{node}/state, per-node discovery, per-node client_id; distinct serial → distinct HA device, collision-free
Metrics DB ✅ samples tagged with the gateway serial
REST 🟡 /api/gateways, /api/gateways/{id}/summary; 47 of 67 endpoints accept ?gateway=
UI 🟡 topbar picker + gwq helper wired into the original tabs

HA entities are minimal: 8 read-only sensors — soc, grid_w, solar_w, battery_w, load_w, generator_w, mode, latency_ms. No switches, selects or numbers, and nothing from any surface added since: no circuits, no BMS cells, no solar detail, no generator.

2. Defect found while surveying — the newest tabs ignore the picker

circuits_tab.js, generator_tab.js, solar_tab.js and battery_tab.js call fetch('api/circuits'), fetch('api/solar') … without ?gateway=. The endpoints all accept it; the tabs never send it. On a multi-gateway site those four tabs silently show the default gateway while the topbar says otherwise — the worst kind of wrong, because it looks right.

Latent on a single-gateway site (this one), which is why tests and live checks never caught it. This is mine — the pattern existed in app.js (gwq) and I did not follow it in four consecutive tabs.

3. Phased plan

Phases are ordered so each ships something usable and nothing depends on a later phase.

Phase 0 — fix the picker regression · S (~half a day)

Component: static/js/*_tab.js

Route the four tabs through the existing gwq helper; add a lint test in the shape of the existing *_tab.js scanner that fails when a tab calls api/… without a gateway parameter, so the next tab cannot regress it. Do this first — it is a correctness bug, not a feature.

Phase 1 — finish REST gateway coverage · S–M (~1 day)

Component: app.py

Add ?gateway= to the remaining ~20 endpoints and assert coverage with a test that walks app.routes and requires the parameter on every /api/ route that reaches a device. That test is the deliverable as much as the parameter is — it converts "we added it everywhere" from a claim into a check.

Phase 2 — HA entities from existing reads · M (~2–3 days)

Component: publish/entities.py + poller state

Additive sensors only, no new device interaction — the poller already reads most of this:

  • Solar — PV total W, energy today, per-input rated kW, relay states
  • Circuits — per-circuit on/off, power, lifetime energy (only for present circuits)
  • Battery — pack SoH, min/max cell mV, cell spread, temperature spread
  • Generator — state, enabled, output W
  • System — grid/solar/generator relays, off-grid flag, standby

Two design rules: never publish an entity for absent hardware (the presence work already decides this), and keep the state topic one JSON document per gateway so entity count does not multiply MQTT traffic.

Phase 3 — writable HA entities · M–L (~3–5 days)

Component: publish/ (new command-topic subscriber) + client.py

Switches and numbers for the writes that are hardware-verified: circuit on/off (switch), operating mode (select), generator enable and config (switch/number). Deliberately excluded: smart-circuit schedules (silently discarded — see DISCARDED_WRITES) and reserve SoC (unproven — RESEARCH-RESERVE-SOC). Every command must go through the existing read-back verification and republish the verified state, never the requested one.

Phase 4 — gateway lifecycle & mock gateways · L (~1 week)

Component: state.py, config.py, new /api/gateways CRUD, Settings UI

Mirror the Modbus Bridge's gateway record: add/edit/remove without a restart, mock gateways for development, per-gateway poll_interval, and the service_id → Utility service link. This is the largest piece and the only one needing persistence changes, which is why it is last despite being requested early — everything above is useful without it.

Phase 5 — per-gateway UI polish · S–M

Gateway switcher in the sidebar as well as the topbar, per-gateway health badges, and a combined "all gateways" dashboard view.

4. Recommendation

Do Phase 0 now regardless of what else is chosen — it is a live correctness bug with a cheap permanent guard.

Then Phase 2 before Phase 1: HA entities are what actually gets used day to day, and the remaining ?gateway= endpoints are mostly diagnostic surfaces that a second gateway would rarely need first. Phase 1 is cheap enough to fold in whenever.

Phase 4 only when a second real gateway exists. Building gateway CRUD against a single-gateway site risks exactly the class of bug found in §2 — untestable paths that look finished. Mock gateways would help, which is an argument for doing them first within Phase 4.