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:
parent
bed2c9e2be
commit
d10e4884b8
9 changed files with 630 additions and 107 deletions
|
|
@ -23,10 +23,21 @@ export async function initDb() {
|
|||
newbook_site_id TEXT UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE, -- soft-deactivate on NewBook sync, never hard delete
|
||||
occupied_temp NUMERIC(4,1) NOT NULL DEFAULT 22.0,
|
||||
vacant_temp NUMERIC(4,1) NOT NULL DEFAULT 16.0,
|
||||
heating_offset_min INT NOT NULL DEFAULT 120, -- minutes before arrival to start heating
|
||||
cooling_offset_min INT NOT NULL DEFAULT -30, -- minutes after departure to stop (negative = before checkout)
|
||||
-- TRV temps + offsets: NULL = inherit the global 'hvac_defaults' config template (see seedDefaults()).
|
||||
-- A non-NULL value here is an explicit per-room override.
|
||||
occupied_temp NUMERIC(4,1),
|
||||
vacant_temp NUMERIC(4,1),
|
||||
heating_offset_min INT, -- minutes before arrival to start heating
|
||||
cooling_offset_min INT, -- minutes after departure to stop (negative = before checkout)
|
||||
-- Aircon template overrides — same inherit-if-NULL relationship to hvac_defaults.ac_occupied/ac_vacant.
|
||||
ac_occupied_power TEXT, -- 'on' | 'off'
|
||||
ac_occupied_mode TEXT, -- cool | heat | fan | auto | dry
|
||||
ac_occupied_setpoint NUMERIC(4,1),
|
||||
ac_occupied_fan_speed TEXT, -- low | medium | high | powerful
|
||||
ac_vacant_power TEXT,
|
||||
ac_vacant_mode TEXT,
|
||||
ac_vacant_setpoint NUMERIC(4,1),
|
||||
ac_vacant_fan_speed TEXT,
|
||||
auto_mode BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
sync_valves BOOLEAN NOT NULL DEFAULT TRUE, -- sync guest setpoint changes across valves in the same zone
|
||||
exclude_bathroom BOOLEAN NOT NULL DEFAULT TRUE, -- bathroom valve excluded from guest-adjustment sync
|
||||
|
|
@ -152,9 +163,41 @@ export async function initDb() {
|
|||
await pool.query(`ALTER TABLE zone_devices ADD COLUMN IF NOT EXISTS remote_locked BOOLEAN`)
|
||||
await pool.query(`ALTER TABLE zone_devices ADD COLUMN IF NOT EXISTS status_updated_at TIMESTAMPTZ`)
|
||||
|
||||
// Default-template rollout: existing installs created occupied_temp/vacant_temp/
|
||||
// heating_offset_min/cooling_offset_min as NOT NULL with a hardcoded default — relax
|
||||
// that so NULL can mean "inherit the global hvac_defaults template" (see seedDefaults()
|
||||
// below). Every existing room's current value is preserved as its explicit override —
|
||||
// nothing changes behaviourally for any room on deploy. DROP NOT NULL/DROP DEFAULT are
|
||||
// no-ops (not errors) if already relaxed, so this stays safe to run on every startup.
|
||||
for (const col of ['occupied_temp', 'vacant_temp', 'heating_offset_min', 'cooling_offset_min']) {
|
||||
await pool.query(`ALTER TABLE zones ALTER COLUMN ${col} DROP NOT NULL`)
|
||||
await pool.query(`ALTER TABLE zones ALTER COLUMN ${col} DROP DEFAULT`)
|
||||
}
|
||||
await pool.query(`ALTER TABLE zones ADD COLUMN IF NOT EXISTS ac_occupied_power TEXT`)
|
||||
await pool.query(`ALTER TABLE zones ADD COLUMN IF NOT EXISTS ac_occupied_mode TEXT`)
|
||||
await pool.query(`ALTER TABLE zones ADD COLUMN IF NOT EXISTS ac_occupied_setpoint NUMERIC(4,1)`)
|
||||
await pool.query(`ALTER TABLE zones ADD COLUMN IF NOT EXISTS ac_occupied_fan_speed TEXT`)
|
||||
await pool.query(`ALTER TABLE zones ADD COLUMN IF NOT EXISTS ac_vacant_power TEXT`)
|
||||
await pool.query(`ALTER TABLE zones ADD COLUMN IF NOT EXISTS ac_vacant_mode TEXT`)
|
||||
await pool.query(`ALTER TABLE zones ADD COLUMN IF NOT EXISTS ac_vacant_setpoint NUMERIC(4,1)`)
|
||||
await pool.query(`ALTER TABLE zones ADD COLUMN IF NOT EXISTS ac_vacant_fan_speed TEXT`)
|
||||
|
||||
await seedDefaults()
|
||||
}
|
||||
|
||||
// Global fallback template for every room's occupied/vacant TRV + aircon state, used
|
||||
// wherever a zone's own override column is NULL. Structured as a single 'hvac_defaults'
|
||||
// config value (not a dedicated table) since the generic config store already holds
|
||||
// arbitrary JSONB per key — see routes/config.js.
|
||||
export const DEFAULT_HVAC_TEMPLATE = {
|
||||
arrival_prep_offset_min: 120, // minutes before arrival to start heating
|
||||
departure_prep_offset_min: -30, // minutes after departure to start cooling (negative = before checkout)
|
||||
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' },
|
||||
}
|
||||
|
||||
async function seedDefaults() {
|
||||
const defaults = {
|
||||
automation_enabled: true, // master kill switch — overrides every zone's own auto_mode
|
||||
|
|
@ -164,6 +207,7 @@ async function seedDefaults() {
|
|||
default_departure_time: '10:00:00',
|
||||
maintenance_url: '',
|
||||
maintenance_api_key: '',
|
||||
hvac_defaults: DEFAULT_HVAC_TEMPLATE,
|
||||
}
|
||||
for (const [key, value] of Object.entries(defaults)) {
|
||||
await pool.query(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue