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>
This commit is contained in:
jtricerolph 2026-08-25 09:48:28 +00:00
parent bed2c9e2be
commit d10e4884b8
9 changed files with 630 additions and 107 deletions

View file

@ -26,6 +26,9 @@ export const DEVICE_TYPE_LABELS: Record<DeviceType, string> = {
// DeviceType is an enum value only, ready for its Phase 3 driver.
export const IMPLEMENTED_DEVICE_TYPES: DeviceType[] = ['shelly_trv', 'mhi_mqtt', 'home_assistant']
// TRV temps/offsets and aircon template fields are all nullable on a zone — NULL means
// "inherit the matching field from hvac_defaults" (see HvacDefaults below); a non-null
// value is that room's explicit override. Set via ZoneDetailModal's per-field toggles.
export interface Zone {
id: number
zone_type: ZoneType
@ -33,10 +36,18 @@ export interface Zone {
newbook_site_id: string | null
name: string
active: boolean
occupied_temp: string
vacant_temp: string
heating_offset_min: number
cooling_offset_min: number
occupied_temp: string | null
vacant_temp: string | null
heating_offset_min: number | null
cooling_offset_min: number | null
ac_occupied_power: 'on' | 'off' | null
ac_occupied_mode: string | null
ac_occupied_setpoint: string | null
ac_occupied_fan_speed: string | null
ac_vacant_power: 'on' | 'off' | null
ac_vacant_mode: string | null
ac_vacant_setpoint: string | null
ac_vacant_fan_speed: string | null
auto_mode: boolean
sync_valves: boolean
exclude_bathroom: boolean
@ -179,6 +190,33 @@ export interface NewbookSite {
excluded: boolean
}
// Global occupied/vacant template — the fallback every room inherits from unless it
// sets its own override (see Zone's nullable ac_*/occupied_temp/vacant_temp fields).
export interface HvacAcTemplate {
power: 'on' | 'off'
mode: string
setpoint: number
fan_speed: string
}
export interface HvacDefaults {
arrival_prep_offset_min: number
departure_prep_offset_min: number
trv_occupied_temp: number
trv_vacant_temp: number
ac_occupied: HvacAcTemplate
ac_vacant: HvacAcTemplate
}
export const DEFAULT_HVAC_TEMPLATE: HvacDefaults = {
arrival_prep_offset_min: 120,
departure_prep_offset_min: -30,
trv_occupied_temp: 22.0,
trv_vacant_temp: 16.0,
ac_occupied: { power: 'on', mode: 'cool', setpoint: 22.0, fan_speed: 'medium' },
ac_vacant: { power: 'off', mode: 'cool', setpoint: 18.0, fan_speed: 'low' },
}
export interface AppConfig {
automation_enabled?: boolean
poll_interval_minutes: number
@ -187,6 +225,7 @@ export interface AppConfig {
maintenance_url: string
maintenance_api_key: string
excluded_site_ids?: string[]
hvac_defaults?: HvacDefaults
}
export interface User {