Commit graph

9 commits

Author SHA1 Message Date
807a27ca1b Fix TRV status ingestion: subscribe to live shellies/+/status topic
The firmware actually running on the deployed TRVs (SHTRV-01 v2.2.4,
confirmed against live hardware) publishes a combined status blob on
shellies/<id>/status, not the per-field shellies/<id>/thermostat/0/target_t
topic handleStatus() was written against — so current_target_temp was
permanently NULL and the dashboard modal always showed "—".

Adds handleStatusBlob() to parse the real payload shape and populate
current_target_temp/current_room_temp/battery_pct. This firmware also has
no guest-vs-automation 'source' field, so guest detection is redesigned to
diff an incoming target_t against a new last_commanded_target_t column,
stamped by publishTarget() right before every command is sent. A mismatch
means the guest changed it via the dial; a match means it's an echo of our
own automation.

Old handleStatus()/topicMatchesStatus() path left in place, dormant —
harmless if some future firmware does use per-field topics.

TRV command topic (shellies/<id>/thermostat/0/command/target_t) is still
unverified against live hardware — flagged in code comments.
2026-08-25 14:09:38 +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
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