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

@ -1,11 +1,56 @@
import { useEffect, useState } from 'react'
import { X, Thermometer, History } from 'lucide-react'
import type { ZoneStatus, ActivityEntry } from '../types'
import { ROOM_STATE_LABELS, can } from '../types'
import type { ZoneStatus, ActivityEntry, HvacDefaults } from '../types'
import { ROOM_STATE_LABELS, DEFAULT_HVAC_TEMPLATE, MHI_MODES, MHI_FAN_SPEEDS, can } from '../types'
import { useAuth } from './AuthGate'
import { updateZone, overrideZone, fetchActivity } from '../api'
import { updateZone, overrideZone, fetchActivity, fetchConfig } from '../api'
import MhiControlPanel from './MhiControlPanel'
// The 12 fields a room can individually override — each is `null` on the zone when
// inheriting the matching field from the global hvac_defaults template (Settings page),
// or an explicit value when overridden. Mirrors the OVERRIDABLE_FIELDS list in
// backend/src/routes/zones.js.
type OverridableField =
| 'occupied_temp' | 'vacant_temp' | 'heating_offset_min' | 'cooling_offset_min'
| 'ac_occupied_power' | 'ac_occupied_mode' | 'ac_occupied_setpoint' | 'ac_occupied_fan_speed'
| 'ac_vacant_power' | 'ac_vacant_mode' | 'ac_vacant_setpoint' | 'ac_vacant_fan_speed'
function defaultFor(field: OverridableField, d: HvacDefaults): string | number {
switch (field) {
case 'occupied_temp': return d.trv_occupied_temp
case 'vacant_temp': return d.trv_vacant_temp
case 'heating_offset_min': return d.arrival_prep_offset_min
case 'cooling_offset_min': return d.departure_prep_offset_min
case 'ac_occupied_power': return d.ac_occupied.power
case 'ac_occupied_mode': return d.ac_occupied.mode
case 'ac_occupied_setpoint': return d.ac_occupied.setpoint
case 'ac_occupied_fan_speed': return d.ac_occupied.fan_speed
case 'ac_vacant_power': return d.ac_vacant.power
case 'ac_vacant_mode': return d.ac_vacant.mode
case 'ac_vacant_setpoint': return d.ac_vacant.setpoint
case 'ac_vacant_fan_speed': return d.ac_vacant.fan_speed
}
}
// Checkbox + either the override input (children) or the inherited default value,
// depending on whether this field is currently overridden.
function OverrideCell({ checked, defaultLabel, onToggle, children }: {
checked: boolean
defaultLabel: string
onToggle: (checked: boolean) => void
children: React.ReactNode
}) {
return (
<div>
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, color: 'var(--text-mid)', marginBottom: 4, cursor: 'pointer' }}>
<input type="checkbox" checked={checked} onChange={e => onToggle(e.target.checked)} style={{ width: 13, height: 13 }} />
Override
</label>
{checked ? children : <span className="muted" style={{ fontSize: 13 }}>{defaultLabel}</span>}
</div>
)
}
export default function ZoneDetailModal({ zone, onClose, onSaved }: {
zone: ZoneStatus
onClose: () => void
@ -15,14 +60,25 @@ export default function ZoneDetailModal({ zone, onClose, onSaved }: {
const canEdit = can(user, 'schedule_edit')
const canControl = can(user, 'control')
const [occupiedTemp, setOccupiedTemp] = useState(zone.occupied_temp)
const [vacantTemp, setVacantTemp] = useState(zone.vacant_temp)
const [heatingOffset, setHeatingOffset] = useState(zone.heating_offset_min)
const [coolingOffset, setCoolingOffset] = useState(zone.cooling_offset_min)
const [hvacDefaults, setHvacDefaults] = useState<HvacDefaults>(DEFAULT_HVAC_TEMPLATE)
const [overrides, setOverrides] = useState<Record<OverridableField, string | number | null>>({
occupied_temp: zone.occupied_temp,
vacant_temp: zone.vacant_temp,
heating_offset_min: zone.heating_offset_min,
cooling_offset_min: zone.cooling_offset_min,
ac_occupied_power: zone.ac_occupied_power,
ac_occupied_mode: zone.ac_occupied_mode,
ac_occupied_setpoint: zone.ac_occupied_setpoint,
ac_occupied_fan_speed: zone.ac_occupied_fan_speed,
ac_vacant_power: zone.ac_vacant_power,
ac_vacant_mode: zone.ac_vacant_mode,
ac_vacant_setpoint: zone.ac_vacant_setpoint,
ac_vacant_fan_speed: zone.ac_vacant_fan_speed,
})
const [autoMode, setAutoMode] = useState(zone.auto_mode)
const [syncValves, setSyncValves] = useState(zone.sync_valves)
const [excludeBathroom, setExcludeBathroom] = useState(zone.exclude_bathroom)
const [overrideTemp, setOverrideTemp] = useState(zone.occupied_temp)
const [overrideTemp, setOverrideTemp] = useState(zone.occupied_temp ?? String(hvacDefaults.trv_occupied_temp))
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const [msg, setMsg] = useState('')
@ -30,16 +86,32 @@ export default function ZoneDetailModal({ zone, onClose, onSaved }: {
useEffect(() => {
fetchActivity(zone.id, 20).then(setActivity).catch(() => {})
fetchConfig().then(cfg => setHvacDefaults(cfg.hvac_defaults ?? DEFAULT_HVAC_TEMPLATE)).catch(() => {})
}, [zone.id])
function toggleOverride(field: OverridableField, checked: boolean) {
setOverrides(o => ({ ...o, [field]: checked ? defaultFor(field, hvacDefaults) : null }))
}
function setOverrideValue(field: OverridableField, value: string | number) {
setOverrides(o => ({ ...o, [field]: value }))
}
async function save() {
setSaving(true); setError(''); setMsg('')
try {
await updateZone(zone.id, {
occupied_temp: Number(occupiedTemp),
vacant_temp: Number(vacantTemp),
heating_offset_min: heatingOffset,
cooling_offset_min: coolingOffset,
occupied_temp: overrides.occupied_temp != null ? Number(overrides.occupied_temp) : null,
vacant_temp: overrides.vacant_temp != null ? Number(overrides.vacant_temp) : null,
heating_offset_min: overrides.heating_offset_min != null ? Number(overrides.heating_offset_min) : null,
cooling_offset_min: overrides.cooling_offset_min != null ? Number(overrides.cooling_offset_min) : null,
ac_occupied_power: overrides.ac_occupied_power || null,
ac_occupied_mode: overrides.ac_occupied_mode || null,
ac_occupied_setpoint: overrides.ac_occupied_setpoint != null ? Number(overrides.ac_occupied_setpoint) : null,
ac_occupied_fan_speed: overrides.ac_occupied_fan_speed || null,
ac_vacant_power: overrides.ac_vacant_power || null,
ac_vacant_mode: overrides.ac_vacant_mode || null,
ac_vacant_setpoint: overrides.ac_vacant_setpoint != null ? Number(overrides.ac_vacant_setpoint) : null,
ac_vacant_fan_speed: overrides.ac_vacant_fan_speed || null,
auto_mode: autoMode,
sync_valves: syncValves,
exclude_bathroom: excludeBathroom,
@ -122,26 +194,182 @@ export default function ZoneDetailModal({ zone, onClose, onSaved }: {
{canEdit && (
<>
<div className="section-title">Schedule</div>
<div className="field-row">
<div className="field">
<label>Occupied temp (°C)</label>
<input type="number" step="0.5" value={occupiedTemp} onChange={e => setOccupiedTemp(e.target.value)} />
</div>
<div className="field">
<label>Vacant temp (°C)</label>
<input type="number" step="0.5" value={vacantTemp} onChange={e => setVacantTemp(e.target.value)} />
</div>
</div>
<p className="field-hint" style={{ marginBottom: 10 }}>
Every value below inherits the global default (Settings → Default Templates) unless overridden here.
</p>
<div className="field-row">
<div className="field">
<label>Heating offset (mins before arrival)</label>
<input type="number" value={heatingOffset} onChange={e => setHeatingOffset(parseInt(e.target.value) || 0)} />
<OverrideCell
checked={overrides.heating_offset_min != null}
defaultLabel={`Default: ${hvacDefaults.arrival_prep_offset_min} mins`}
onToggle={c => toggleOverride('heating_offset_min', c)}
>
<input
type="number" value={overrides.heating_offset_min ?? 0}
onChange={e => setOverrideValue('heating_offset_min', parseInt(e.target.value) || 0)}
/>
</OverrideCell>
</div>
<div className="field">
<label>Cooling offset (mins after departure)</label>
<input type="number" value={coolingOffset} onChange={e => setCoolingOffset(parseInt(e.target.value) || 0)} />
<OverrideCell
checked={overrides.cooling_offset_min != null}
defaultLabel={`Default: ${hvacDefaults.departure_prep_offset_min} mins`}
onToggle={c => toggleOverride('cooling_offset_min', c)}
>
<input
type="number" value={overrides.cooling_offset_min ?? 0}
onChange={e => setOverrideValue('cooling_offset_min', parseInt(e.target.value) || 0)}
/>
</OverrideCell>
</div>
</div>
<div style={{ fontWeight: 600, fontSize: 13, margin: '10px 0 6px' }}>Aircon</div>
<div className="table-wrap" style={{ marginBottom: 16 }}>
<table className="data">
<thead><tr><th></th><th>Occupied</th><th>Vacant</th></tr></thead>
<tbody>
<tr>
<td>On/Off</td>
<td>
<OverrideCell
checked={overrides.ac_occupied_power != null}
defaultLabel={hvacDefaults.ac_occupied.power === 'on' ? 'Default: On' : 'Default: Off'}
onToggle={c => toggleOverride('ac_occupied_power', c)}
>
<select value={overrides.ac_occupied_power ?? 'on'} onChange={e => setOverrideValue('ac_occupied_power', e.target.value)}>
<option value="on">On</option><option value="off">Off</option>
</select>
</OverrideCell>
</td>
<td>
<OverrideCell
checked={overrides.ac_vacant_power != null}
defaultLabel={hvacDefaults.ac_vacant.power === 'on' ? 'Default: On' : 'Default: Off'}
onToggle={c => toggleOverride('ac_vacant_power', c)}
>
<select value={overrides.ac_vacant_power ?? 'off'} onChange={e => setOverrideValue('ac_vacant_power', e.target.value)}>
<option value="on">On</option><option value="off">Off</option>
</select>
</OverrideCell>
</td>
</tr>
<tr>
<td>Mode</td>
<td>
<OverrideCell
checked={overrides.ac_occupied_mode != null}
defaultLabel={`Default: ${hvacDefaults.ac_occupied.mode}`}
onToggle={c => toggleOverride('ac_occupied_mode', c)}
>
<select value={overrides.ac_occupied_mode ?? 'cool'} onChange={e => setOverrideValue('ac_occupied_mode', e.target.value)}>
{MHI_MODES.map(m => <option key={m} value={m}>{m[0].toUpperCase() + m.slice(1)}</option>)}
</select>
</OverrideCell>
</td>
<td>
<OverrideCell
checked={overrides.ac_vacant_mode != null}
defaultLabel={`Default: ${hvacDefaults.ac_vacant.mode}`}
onToggle={c => toggleOverride('ac_vacant_mode', c)}
>
<select value={overrides.ac_vacant_mode ?? 'cool'} onChange={e => setOverrideValue('ac_vacant_mode', e.target.value)}>
{MHI_MODES.map(m => <option key={m} value={m}>{m[0].toUpperCase() + m.slice(1)}</option>)}
</select>
</OverrideCell>
</td>
</tr>
<tr>
<td>Setpoint (°C)</td>
<td>
<OverrideCell
checked={overrides.ac_occupied_setpoint != null}
defaultLabel={`Default: ${hvacDefaults.ac_occupied.setpoint}°C`}
onToggle={c => toggleOverride('ac_occupied_setpoint', c)}
>
<input
type="number" step="0.5" style={{ width: 90 }}
value={overrides.ac_occupied_setpoint ?? 0}
onChange={e => setOverrideValue('ac_occupied_setpoint', parseFloat(e.target.value) || 0)}
/>
</OverrideCell>
</td>
<td>
<OverrideCell
checked={overrides.ac_vacant_setpoint != null}
defaultLabel={`Default: ${hvacDefaults.ac_vacant.setpoint}°C`}
onToggle={c => toggleOverride('ac_vacant_setpoint', c)}
>
<input
type="number" step="0.5" style={{ width: 90 }}
value={overrides.ac_vacant_setpoint ?? 0}
onChange={e => setOverrideValue('ac_vacant_setpoint', parseFloat(e.target.value) || 0)}
/>
</OverrideCell>
</td>
</tr>
<tr>
<td>Fan speed</td>
<td>
<OverrideCell
checked={overrides.ac_occupied_fan_speed != null}
defaultLabel={`Default: ${hvacDefaults.ac_occupied.fan_speed}`}
onToggle={c => toggleOverride('ac_occupied_fan_speed', c)}
>
<select value={overrides.ac_occupied_fan_speed ?? 'medium'} onChange={e => setOverrideValue('ac_occupied_fan_speed', e.target.value)}>
{MHI_FAN_SPEEDS.map(f => <option key={f} value={f}>{f[0].toUpperCase() + f.slice(1)}</option>)}
</select>
</OverrideCell>
</td>
<td>
<OverrideCell
checked={overrides.ac_vacant_fan_speed != null}
defaultLabel={`Default: ${hvacDefaults.ac_vacant.fan_speed}`}
onToggle={c => toggleOverride('ac_vacant_fan_speed', c)}
>
<select value={overrides.ac_vacant_fan_speed ?? 'low'} onChange={e => setOverrideValue('ac_vacant_fan_speed', e.target.value)}>
{MHI_FAN_SPEEDS.map(f => <option key={f} value={f}>{f[0].toUpperCase() + f.slice(1)}</option>)}
</select>
</OverrideCell>
</td>
</tr>
</tbody>
</table>
</div>
<div style={{ fontWeight: 600, fontSize: 13, margin: '10px 0 6px' }}>TRVs</div>
<div className="field-row">
<div className="field">
<label>Occupied setpoint (°C)</label>
<OverrideCell
checked={overrides.occupied_temp != null}
defaultLabel={`Default: ${hvacDefaults.trv_occupied_temp}°C`}
onToggle={c => toggleOverride('occupied_temp', c)}
>
<input
type="number" step="0.5" value={overrides.occupied_temp ?? 0}
onChange={e => setOverrideValue('occupied_temp', e.target.value)}
/>
</OverrideCell>
</div>
<div className="field">
<label>Vacant setpoint (°C)</label>
<OverrideCell
checked={overrides.vacant_temp != null}
defaultLabel={`Default: ${hvacDefaults.trv_vacant_temp}°C`}
onToggle={c => toggleOverride('vacant_temp', c)}
>
<input
type="number" step="0.5" value={overrides.vacant_temp ?? 0}
onChange={e => setOverrideValue('vacant_temp', e.target.value)}
/>
</OverrideCell>
</div>
</div>
<label className="field-check">
<input type="checkbox" checked={autoMode} onChange={e => setAutoMode(e.target.checked)} />
Auto mode (NewBook-driven scheduling)

View file

@ -4,7 +4,8 @@ import {
fetchNewbookSites, saveExcludedSites, testNewbookConnection, syncZonesFromNewbook,
fetchConfig, updateConfig, fetchMqttStatus, createZone,
} from '../api'
import type { NewbookSite, AppConfig } from '../types'
import type { NewbookSite, AppConfig, HvacDefaults, HvacAcTemplate } from '../types'
import { DEFAULT_HVAC_TEMPLATE, MHI_MODES, MHI_FAN_SPEEDS } from '../types'
export default function Settings() {
const [sites, setSites] = useState<NewbookSite[]>([])
@ -24,6 +25,11 @@ export default function Settings() {
const [maintKey, setMaintKey] = useState('')
const [configSaving, setConfigSaving] = useState(false)
// Default occupied/vacant templates — every room inherits these unless it sets its
// own override (ZoneDetailModal's per-field "Override" toggles).
const [defaults, setDefaults] = useState<HvacDefaults>(DEFAULT_HVAC_TEMPLATE)
const [defaultsSaving, setDefaultsSaving] = useState(false)
const [mqttConnected, setMqttConnected] = useState<boolean | null>(null)
const [mqttNote, setMqttNote] = useState('')
@ -47,6 +53,7 @@ export default function Settings() {
setDefaultDeparture(cfg.default_departure_time ?? '10:00:00')
setMaintUrl(cfg.maintenance_url ?? '')
setMaintKey(cfg.maintenance_api_key ?? '')
setDefaults(cfg.hvac_defaults ?? DEFAULT_HVAC_TEMPLATE)
}).catch(() => {})
fetchMqttStatus().then(s => { setMqttConnected(s.connected); setMqttNote(s.note) }).catch(() => {})
}, [])
@ -122,6 +129,22 @@ export default function Settings() {
}
}
function updateAcDefault(which: 'ac_occupied' | 'ac_vacant', field: keyof HvacAcTemplate, value: string | number) {
setDefaults(d => ({ ...d, [which]: { ...d[which], [field]: value } }))
}
async function saveDefaults() {
setDefaultsSaving(true); setError(''); setMsg('')
try {
await updateConfig('hvac_defaults', defaults)
setMsg('Default templates saved'); setTimeout(() => setMsg(''), 2500)
} catch (e) {
setError(e instanceof Error ? e.message : 'Save failed')
} finally {
setDefaultsSaving(false)
}
}
async function handleCreatePublicArea(e: React.FormEvent) {
e.preventDefault()
if (!publicAreaName.trim()) return
@ -226,6 +249,103 @@ export default function Settings() {
</div>
</div>
<div className="section-title">Default Templates</div>
<p className="field-hint" style={{ marginBottom: 10 }}>
Fallback occupied/vacant state for every room's TRVs and aircon. Any room can override
individual values on its own card (Dashboard → click the room → Schedule section).
</p>
<div className="field-row">
<div className="field">
<label>Default arrival preparing offset (mins)</label>
<input
type="number" value={defaults.arrival_prep_offset_min}
onChange={e => setDefaults(d => ({ ...d, arrival_prep_offset_min: parseInt(e.target.value) || 0 }))}
/>
</div>
<div className="field">
<label>Default departing preparation offset (mins)</label>
<input
type="number" value={defaults.departure_prep_offset_min}
onChange={e => setDefaults(d => ({ ...d, departure_prep_offset_min: parseInt(e.target.value) || 0 }))}
/>
</div>
</div>
<div style={{ fontWeight: 600, fontSize: 13, margin: '10px 0 6px' }}>Default Aircon</div>
<div className="table-wrap" style={{ marginBottom: 16 }}>
<table className="data">
<thead><tr><th></th><th>Occupied</th><th>Vacant</th></tr></thead>
<tbody>
<tr>
<td>On/Off</td>
{(['ac_occupied', 'ac_vacant'] as const).map(which => (
<td key={which}>
<select value={defaults[which].power} onChange={e => updateAcDefault(which, 'power', e.target.value)}>
<option value="on">On</option>
<option value="off">Off</option>
</select>
</td>
))}
</tr>
<tr>
<td>Mode</td>
{(['ac_occupied', 'ac_vacant'] as const).map(which => (
<td key={which}>
<select value={defaults[which].mode} onChange={e => updateAcDefault(which, 'mode', e.target.value)}>
{MHI_MODES.map(m => <option key={m} value={m}>{m[0].toUpperCase() + m.slice(1)}</option>)}
</select>
</td>
))}
</tr>
<tr>
<td>Setpoint (°C)</td>
{(['ac_occupied', 'ac_vacant'] as const).map(which => (
<td key={which}>
<input
type="number" step="0.5" style={{ width: 90 }}
value={defaults[which].setpoint}
onChange={e => updateAcDefault(which, 'setpoint', parseFloat(e.target.value) || 0)}
/>
</td>
))}
</tr>
<tr>
<td>Fan speed</td>
{(['ac_occupied', 'ac_vacant'] as const).map(which => (
<td key={which}>
<select value={defaults[which].fan_speed} onChange={e => updateAcDefault(which, 'fan_speed', e.target.value)}>
{MHI_FAN_SPEEDS.map(f => <option key={f} value={f}>{f[0].toUpperCase() + f.slice(1)}</option>)}
</select>
</td>
))}
</tr>
</tbody>
</table>
</div>
<div style={{ fontWeight: 600, fontSize: 13, margin: '10px 0 6px' }}>Default TRVs</div>
<div className="field-row">
<div className="field">
<label>Occupied setpoint (°C)</label>
<input
type="number" step="0.5" value={defaults.trv_occupied_temp}
onChange={e => setDefaults(d => ({ ...d, trv_occupied_temp: parseFloat(e.target.value) || 0 }))}
/>
</div>
<div className="field">
<label>Vacant setpoint (°C)</label>
<input
type="number" step="0.5" value={defaults.trv_vacant_temp}
onChange={e => setDefaults(d => ({ ...d, trv_vacant_temp: parseFloat(e.target.value) || 0 }))}
/>
</div>
</div>
<div style={{ marginBottom: 24 }}>
<button className="btn btn-primary" disabled={defaultsSaving} onClick={saveDefaults}>
{defaultsSaving ? 'Saving…' : 'Save Default Templates'}
</button>
</div>
<div className="section-title">Maintenance Integration</div>
<p className="field-hint" style={{ marginBottom: 10 }}>
Not yet available — maintenance needs its own Settings → API Keys page first.

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 {