Commit graph

15 commits

Author SHA1 Message Date
2db7d5f4d0 Add kiosk wall-panel control view
Full-screen /kiosk route, separate from the staff SPA: dedicated long-lived
kiosk login (own password, hashed, mints its own hnf_session cookie locally
via CENTRAL_AUTH_SECRET -- no central auth changes needed), scrollable grid
of small mode/setpoint blocks per live-controllable device (shelly_trv,
mhi_mqtt, daikin), tap-to-edit modal reusing the existing per-type control
panels, and a type-aware bulk multiselect (separate TRV/AC sections, only
applied to matching selected devices, TRV commands staggered).

- backend/src/routes/kiosk.js: public POST /api/kiosk/login
- backend/src/routes/settings.js: admin set/check kiosk password (bcrypt)
- backend/src/routes/config.js: exclude/protect kiosk_password_hash
- frontend/src/pages/Kiosk.tsx + components/Kiosk*: the kiosk UI
- reuses GET /api/status for the combined device list -- no new status
  endpoint needed since the kiosk token now also carries the 'view' cap

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 20:44:50 +00:00
f0b4ac3c41 Add device telemetry history + per-device graph view
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).
2026-08-25 17:28:45 +00:00
d818bd1e7a Add per-TRV control panel + valve/wifi telemetry, restyle dashboard radiator icon
Backend:
- New valve_position column, populated from shellies/<id>/info alongside
  wifi_rssi (was already a column, never actually written to).
- New GET/POST /api/devices/:id/trv-status and /trv-control routes, mirroring
  the mhi-status/mhi-control and daikin-status/daikin-control pattern. Control
  goes through the existing setTargetWithRetry() reliability pipeline.

Frontend:
- New TrvControlPanel.tsx: +/- setpoint control debounced 1s after the last
  click before firing a command (these are battery/deep-sleep valves going
  through a real retry/backoff pipeline, not a cheap register write like MHI/
  Daikin), plus room temp, valve position %, wifi signal, and a live "time
  since last update" readout. Wired into ZoneDetailModal for shelly_trv rows.
- ZoneCard dashboard tile: radiator icon now just shows red when heating_active
  (valve open) / grey when idle, with only the target setpoint alongside it
  (dropped the room-temp half, redundant with the zone-level temp line above).
2026-08-25 14:25:46 +00:00
373f66ef5c Add Daikin local-API driver — MAC-matched subnet sweep instead of UDP broadcast
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>
2026-08-25 12:01:58 +00:00
5fc576aabd Show weather compensation summary banner on the Dashboard
New lib/ac-template.js factors the "which occupied AC template applies
right now" resolution out of scheduler.js (dedupes the tier-pick loop
that used to live separately in scheduler.js and status.js) into a
shared resolveOccupiedAcTier()/resolveGlobalOccupiedAcTemplate() pair.
GET /api/status now returns active_ac_template alongside weather_peak_c.

Dashboard shows a small banner above the zone grid: "Forecast high
today: XC" + "Aircon default: ON, 20C, High fan — Warm tier" — so staff
don't have to visit Settings to see what today's weather tier resolved
to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 11:42:01 +00:00
dc8c9df719 Fix: a booking arriving in a few days showed 'booked' instead of 'vacant'
resolveGoverningBooking() was letting an upcoming (not-today) booking govern colour,
so any room with something booked within the 7-day lookahead showed green 'Booked'
regardless of how far off the arrival was. 'booked' should only apply when someone's
actually arriving tonight.

Now a booking only governs state/colour when it covers today (arriving today,
mid-stay, or departing today) — otherwise the room is genuinely vacant. The soonest
upcoming booking (if any) is surfaced separately as vacant_nights, a countdown to
the next arrival, purely informational and no longer tied to colour. Dashboard shows
'Vacant N nights' when something's booked within the window, or 'Vacant 7+ nights'
when nothing was found in the fetched window at all.

New zone_state.vacant_nights column, threaded through saveZoneState/pollOnce/status.js.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 11:10:35 +00:00
915fc0039c Add weather compensation tiers for occupied aircon defaults
Warm/hot/heatwave tiers (ascending peak_above_c thresholds) override the
global ac_occupied template when today's forecast peak clears one, picked
once at the occupied-state transition so a guest's later manual adjustment
is never stomped. New lib/weather.js fetches the peak via Met Office
DataHub (if configured in settings) or Open-Meteo (no key needed) as a
fallback, TTL-cached with a stale-fallback window on fetch failure.
Zone-level per-field overrides still apply on top of a picked tier.

Thresholds/templates live in hvac_defaults.ac_occupied_tiers (existing
JSONB config blob, no migration) and are editable on the Settings page's
new Weather Compensation table. Hotel lat/lon and the optional Met Office
API key live centrally in the settings app (hotel_location/met_office
integrations), not hvac-local, for reuse by other apps later.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 11:01:41 +00:00
48ffebc08d Widen booking fetch window, resolve governing booking per-night, add turnover icons/nights to dashboard
Backend:
- fetchBookings() window widened from today-only to yesterday..+7 days, so a
  departing booking (arrived any number of nights ago) and upcoming bookings are
  both in view — the previous today-only fetch couldn't see a departing guest's
  arrival date at all.
- New resolveGoverningBooking(): replaces the single-booking-per-site pick with a
  two-tier resolution — (1) any non-cancelled booking whose stay covers today,
  preferring one not yet marked 'departed' (so a departing guest keeps governing
  blue->purple right up until checkout is confirmed, even with a same-day arrival
  also active); (2) otherwise the soonest upcoming booking, purely for colour
  ('booked' ahead of its own prep window). Also computes booking_kind
  (arrival/departure/stayover), current/total night count, and a has_next_booking
  turnover hint (for departure days with another booking already lined up).
- determineRoomState() no longer forces 'vacant' for a future-dated arrival — that
  case is now handled by resolveGoverningBooking() deciding whether a booking is
  relevant to today at all; once picked as governing it always resolves to at
  least 'booked'.
- zone_state table: new booking_kind/current_night/total_nights/has_next_booking
  columns, persisted every poll alongside room_state and exposed via GET /api/status.

Frontend:
- ZoneCard: room name is now followed by a takeoff (departure) / landing (arrival)
  / bed (stayover) icon plus a current/total nights figure, sourced from the new
  status fields. A departure additionally shows a landing icon when has_next_booking,
  hinting at a same-day turnover before the room's actually marked departed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:21:16 +00:00
d10e4884b8 Add global occupied/vacant default templates with per-room overrides
Settings gains a 'Default Templates' section: arrival-prep/departure-prep offsets,
a default aircon template (on/off, mode, setpoint, fan speed x occupied/vacant), and
default TRV setpoints. Every room can individually override any of these fields via
per-field 'Override' toggles in its Schedule modal, falling back to the global
default when not overridden.

Backend:
- zones table: occupied_temp/vacant_temp/heating_offset_min/cooling_offset_min are
  now nullable (NULL = inherit default); existing values preserved as explicit
  per-room overrides, so no room's behaviour changes on deploy. 8 new nullable
  ac_occupied_*/ac_vacant_* override columns.
- New 'hvac_defaults' config key (generic config store) holds the global template.
- scheduler.js: resolves each field as zone-override ?? global-default, and now
  also drives AC state automatically on occupancy transitions (previously AC was
  100% manual) via the same command path the manual control panel uses.
- lib/ac-control.js: factored the mhi_mqtt/mhi_modbus command logic out of
  routes/override.js so the scheduler and the manual control route share one path.
- zones.js PATCH: explicit-null-clears-override semantics for the 12 overridable
  fields (distinct from omitting the key, which leaves the value unchanged).

Frontend:
- Settings.tsx: new Default Templates section matching the requested layout.
- ZoneDetailModal.tsx: per-field Override checkboxes for offsets, AC
  occupied/vacant template, and TRV occupied/vacant setpoints.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 09:48:28 +00:00
bed2c9e2be Dashboard: drop room-state badge, colour left border to match NewBook status colours
- Remove status badge from ZoneCard, keep left-border indicator only (with hover title for accessibility)
- Rename heating_up label from 'Heating Up' to 'Preparing'
- Recolour room states: booked=green, occupied=blue, cooling_down=purple (matches NewBook's status colours); vacant stays grey, heating_up stays amber
- Decouple rad-icon.heating from --st-occupied (now blue) to --st-heating-up (amber) so an actively-heating radiator icon still reads warm

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 09:24:54 +00:00
c873e4a270 hvac: native MQTT driver for MHI aircon (replaces Modbus for control/status)
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>
2026-08-13 13:31:01 +00:00
0929285651 Redesign dashboard zone card with live temps, AC status, and per-radiator state
Adds actual room-temperature capture for TRVs over MQTT and a background
Modbus poller for AC mode/fan/lock/room-temp, so the dashboard tile can show
real setpoint-vs-actual readings, an AC status block, and up to 3 radiator
icons per zone instead of just the setpoint.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 20:25:50 +00:00
c6e3c73ead Add master automation on/off switch to Settings
A single global toggle that overrides every zone's own auto_mode —
lets staff pause all HVAC automation stack-wide (e.g. during
maintenance) without touching each zone individually. Backed by the
existing config key/value store; pollOnce() short-circuits entirely
when off (no NewBook fetch, no state tracking, no device commands).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 15:46:12 +00:00
e5c40bbb8c Add MHI aircon manual control (Phase 2, Modbus only)
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>
2026-07-27 14:43:38 +00:00
276c04f8c6 Scaffold Phase 1 hvac app — NewBook-driven room TRV heating scheduler
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>
2026-07-26 18:00:42 +00:00