activity_log only ever recorded state transitions/overrides/sync/errors —
useful for "what did the app decide" but not "what was the room actually
doing" when a guest/staff complaint comes in after the fact.
Backend:
- New device_readings time-series table (room_temp, target_temp, mode,
power_state, fan_speed, valve_position, heating_active), throttled to
~1 row/device/minute via recordReading() in db.js regardless of how
often a caller fires — MQTT pushes can be far chattier than that.
- Wired into every place zone_devices' live-status cache already gets
updated: ac-poller.js, daikin-poller.js, and mqtt.js's TRV
(handleStatusBlob/handleInfo) and native-MQTT MHI (writeMhiState)
handlers.
- 4-day retention by default (config key history_retention_days),
pruned every 6h by the new history-pruner.js.
- GET /api/devices/:id/history?hours=96 to read it back.
Frontend:
- DeviceHistoryModal: room temp vs setpoint line chart, plus a
valve-position chart (TRVs) or fan-speed step chart (AC), plus a
diffed mode/power/fan change log — 24h/4d/7d range picker. Uses
recharts, matching the convention already established in
utilities/wages/reports.
- Graph icon on each device in both ZoneDetailModal and the Devices
page opens it.
Side effect: frontend/package-lock.json was missing vite-plugin-pwa's
whole dependency tree even though package.json already declared it —
pre-existing drift, reconciled by this npm install (unrelated to the
recharts addition, just came along with it).
Daikin BRP069/BRP072-class units are WiFi/DHCP with no fixed IP. hvac's backend
runs as a Docker container on its own bridge network (not directly on the LXC's
admin-VLAN NIC), so UDP broadcast discovery risked silently failing to egress —
instead, every unit is identified by MAC (external_ref), and a background poller
periodically HTTP-sweeps the configured subnet CIDR and matches replies by MAC,
self-healing zone_devices.device_ip whenever a lease changes. Only needs plain
outbound HTTP, same path already proven by the MHI Modbus TCP driver.
- lib/drivers/daikin.js: discover()/getStatus()/setTarget() against the local
/common/basic_info + /aircon/get_control_info + /aircon/set_control_info API
(confirmed live against firmware 4_2_303, 2026-08-25). Mode/fan-rate encoding
taken from community reverse-engineering, not yet verified against a live
setTarget() call — flagged in comments for first real control test.
- lib/daikin-poller.js: 60s status poll (cached IP) + 5min subnet resweep
(self-heal IP + auto-register new units, never auto-assigned to a zone).
- lib/device-registry.js: shared discover-upsert, extracted so both the manual
Discover button and the background sweep record device_ip the same way.
- ac-control.js / routes/override.js: daikin-status/daikin-control endpoints
and commandAcDevice() branch, mirroring the existing MHI control path.
- Settings page: new "Daikin subnet (CIDR)" field (daikin_subnet_cidr config
key) — sweeps do nothing until this is set.
- Frontend: DaikinControlPanel.tsx (on/off/mode/setpoint/fan), wired into
ZoneDetailModal; daikin moved from NOT_YET_IMPLEMENTED to a real driver.
Backend node --check and frontend tsc --noEmit both clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was no way to remove a zone_devices mapping — a gap exposed by the
Modbus->MQTT switchover, which leaves the old mhi_modbus rows stale beside
the freshly auto-registered mhi_mqtt ones. Adds DELETE /api/devices/:id
(manage_devices cap, unlinks photo files then deletes; device_photos
cascade) and a 'Delete device' button in the expanded device row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Intesis MHI gateway was switched from Modbus TCP to native MQTT mode
(2026-08-13), publishing retained per-signal JSON status to
hvac/mhi/IU<nn>/status/<signal> and subscribing to commands at
hvac/mhi/IU<nn>/cmd/<signal>Cmd. This adds an MQTT ingest + command path
alongside the now-dormant Modbus driver, under a new mhi_mqtt device type.
Backend:
- lib/mqtt.js: subscribe hvac/mhi/+/status/+, parse JSON payloads, auto-
register each unit as an unassigned mhi_mqtt zone_devices row and cache
live status (power/mode/setpoint/room temp/fan/health). Add
publishMhiCommand() for on/off/mode/setpoint/fan/vanes writes (QoS 1,
retain off — a retained command would replay stale on gateway reconnect).
Numeric mode/fan enums map index-for-index to the frontend label arrays.
- routes/override.js: mhi-status/mhi-control branch on device_type —
mhi_mqtt serves from the push-populated cache and publishes commands;
mhi_modbus keeps its synchronous register path unchanged.
- routes/devices.js: discover() note for mhi_mqtt (units self-register).
- No DB migration: existing status columns cover it.
Credentials: getCredentials() now prefers env-injected creds
(MQTT_BROKER_HOST/USERNAME/PASSWORD) over the settings fetch — the broker
hashes each dynsec client password show-once, so it can't be re-served from
settings at runtime. Compose passes the new env through.
Frontend: add mhi_mqtt to DeviceType/labels/implemented list, render
MhiControlPanel and count it as an AC device type.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Manual on/off/mode/setpoint/fan control for MHI indoor units via the
already-verified Intesis Modbus TCP gateway, deliberately NOT wired into
the NewBook-driven scheduler yet — that stays deferred. Modbus doesn't
depend on the MQTT broker (separate infra, still unbuilt), so this can be
deployed and tested standalone.
- mhi_gateways table (connection config: host/port/slave id/address base)
+ zone_devices columns for MHI devices (gateway id, unit index, IU hint,
and the per-device register map staged from a MAPS import — ground
truth, never re-derived from room/IU at runtime)
- drivers/mhi-modbus.js: discover/getStatus/setTarget over modbus-serial,
same interface as trv.js/homeassistant.js; per-gateway request queue
since Modbus TCP requires serialised requests; falls back to the
profile's stride formula (with a loud warning) only if a unit has never
been through an import
- routes/mhi-gateways.js: gateway CRUD, live test-connection, xlsx import
(stages the parsed result for review — never auto-creates devices),
per-unit 'assign to zone' as the explicit commit step
- routes/override.js: GET .../mhi-status, POST .../mhi-control (control cap)
- frontend: GatewaysPanel (add gateway, test connection, import + preview,
assign units) and MhiControlPanel (on/off, mode, setpoint, fan) wired
into Devices.tsx / ZoneDetailModal.tsx
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports the state machine, retry/backoff, and guest-override detection from
the retired homeassistant-newbook-heating-component, without depending on
Home Assistant. Backend (Fastify/pg) + frontend (React/Vite/TS) following
standard stack conventions; LXC 128 (127 was already taken by utilities).
MHI/Midea/Daikin/boiler drivers and the shared MQTT broker (LXC 104) are
later phases/infra, not included here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>