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>
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>
- 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>
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>
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>
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>