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>
pollOnce() previously collapsed multiple bookings overlapping today for one site
down to a single candidate before computing room_state, with a weak tie-break
(prefer 'arrived' status, else whichever the API happened to return first). On a
same-day turnover — departing guest + incoming guest both returned by NewBook's
'staying' list for the same site — if neither was marked 'arrived' yet, this could
arbitrarily lock onto the incoming guest's booking (still in 'booked', prep not
started) instead of the departing guest's (which should be 'cooling_down'), even
though the departing guest was the one actually still checked into the room.
Now every candidate booking for a site is evaluated (each zone applies its own
offsets, so this only resolves once we have the zone), and the state machine picks
whichever produces the most active result: occupied > heating_up > cooling_down >
booked > vacant. Zero behaviour change for the common single-booking-per-site case.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
- 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>
There was no way to remove an mhi_gateways row either, alongside the
already-fixed device-delete gap — exposed by the same Modbus->MQTT
switchover. Adds DELETE /api/mhi-gateways/:id (manage_devices cap) and a
'Delete gateway' button. Devices referencing the gateway are NOT deleted
(mhi_gateway_id just goes NULL per the existing ON DELETE SET NULL FK) —
the confirm dialog says so and points at the separate device-delete flow
for retiring the stale mhi_modbus rows too.
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
getCredentials() now fetches the hvac-backend broker login from Settings'
new internal service-client endpoint (GET /internal/mqtt-client/hvac-backend,
bearer SETTINGS_SECRET) — same runtime-fetch pattern as newbook.js, no
broker secret in this app's .env. Env MQTT_* vars remain a dev-only override.
Drops the MQTT_USERNAME/PASSWORD compose passthrough added earlier.
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>
iOS Safari has no beforeinstallprompt API, so the portal's Install
button (?install=1) silently did nothing there. IosInstallHint now
shows Share -> Add to Home Screen steps on Safari, or a prompt to
switch to Safari first if opened in another iOS browser/in-app
webview (those can't install PWAs on iOS at all).
Also added apple-mobile-web-app-title + apple-touch-icon so the
home screen icon isn't a page screenshot, and added the
beforeinstallprompt handler that was missing entirely (Android
Install button did nothing either).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Master switch should behave like each zone's own auto_mode — NewBook
polling, room-state tracking, and activity logging keep running as
normal; only the actual device setpoint commands are held back.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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>
standalone connection test script
Register map derived and validated from two real Intesis MAPS signals
exports (2-unit and 11-unit) taken from the physically installed gateway.
The 11-unit export overturned an earlier assumption from the config
screen's "Fixed" addressing label: register slots are dense in
config/commission order, not fixed-per-IU with gaps — so a unit's
register base must come from the imported map (or be entered manually),
never computed from room number or SuperLink IU address.
- drivers/mhi-profiles/intesis-mhi-modbus.js: register map + encodings for
the confirmed gateway (TCP, port 502, slave 1, single-slave mode),
documenting the corrected slot-vs-IU distinction
- lib/mhi-xlsx-import.js: parses an Intesis MAPS signals export into a
structured per-unit register map (0 unmatched rows against both real
samples) — this becomes the authoritative Phase 2 source of truth,
with the static profile only a pre-first-export fallback
- scripts/test-modbus-connection.py: dependency-free Modbus TCP test
(validated against a local loopback mock server) to resolve the one
remaining open question — the address-base convention — directly from
the Proxmox host, which already sits on the trusted admin VLAN
- docs/: both real MAPS export samples, preserved as provenance
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>