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>
This commit is contained in:
parent
e5c40bbb8c
commit
c6e3c73ead
4 changed files with 46 additions and 0 deletions
|
|
@ -146,6 +146,7 @@ export async function initDb() {
|
||||||
|
|
||||||
async function seedDefaults() {
|
async function seedDefaults() {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
|
automation_enabled: true, // master kill switch — overrides every zone's own auto_mode
|
||||||
excluded_site_ids: [],
|
excluded_site_ids: [],
|
||||||
poll_interval_minutes: 10, // matches the old integration's DEFAULT_SCAN_INTERVAL
|
poll_interval_minutes: 10, // matches the old integration's DEFAULT_SCAN_INTERVAL
|
||||||
default_arrival_time: '15:00:00',
|
default_arrival_time: '15:00:00',
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,12 @@ async function saveZoneState(zoneId, roomState, bookingStatus) {
|
||||||
// One poll cycle: fetch bookings once, walk every active room zone, apply heating
|
// One poll cycle: fetch bookings once, walk every active room zone, apply heating
|
||||||
// logic only where the room's state has actually changed since the last cycle.
|
// logic only where the room's state has actually changed since the last cycle.
|
||||||
export async function pollOnce() {
|
export async function pollOnce() {
|
||||||
|
// Master kill switch — overrides every zone's own auto_mode. Off means a full
|
||||||
|
// pause: no NewBook fetch, no state tracking, no device commands. Deliberately
|
||||||
|
// silent (no activity_log entry) since "paused" is a normal standing state, not
|
||||||
|
// an event — the Settings toggle itself is the record of when it changed.
|
||||||
|
if (!(await getConfig('automation_enabled', true))) return
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
default_arrival_time: await getConfig('default_arrival_time', '15:00:00'),
|
default_arrival_time: await getConfig('default_arrival_time', '15:00:00'),
|
||||||
default_departure_time: await getConfig('default_departure_time', '10:00:00'),
|
default_departure_time: await getConfig('default_departure_time', '10:00:00'),
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ export default function Settings() {
|
||||||
const [syncing, setSyncing] = useState(false)
|
const [syncing, setSyncing] = useState(false)
|
||||||
|
|
||||||
const [config, setConfig] = useState<AppConfig | null>(null)
|
const [config, setConfig] = useState<AppConfig | null>(null)
|
||||||
|
const [automationEnabled, setAutomationEnabled] = useState(true)
|
||||||
|
const [automationSaving, setAutomationSaving] = useState(false)
|
||||||
const [pollMinutes, setPollMinutes] = useState(10)
|
const [pollMinutes, setPollMinutes] = useState(10)
|
||||||
const [defaultArrival, setDefaultArrival] = useState('15:00:00')
|
const [defaultArrival, setDefaultArrival] = useState('15:00:00')
|
||||||
const [defaultDeparture, setDefaultDeparture] = useState('10:00:00')
|
const [defaultDeparture, setDefaultDeparture] = useState('10:00:00')
|
||||||
|
|
@ -39,6 +41,7 @@ export default function Settings() {
|
||||||
loadSites()
|
loadSites()
|
||||||
fetchConfig().then(cfg => {
|
fetchConfig().then(cfg => {
|
||||||
setConfig(cfg)
|
setConfig(cfg)
|
||||||
|
setAutomationEnabled(cfg.automation_enabled ?? true)
|
||||||
setPollMinutes(cfg.poll_interval_minutes ?? 10)
|
setPollMinutes(cfg.poll_interval_minutes ?? 10)
|
||||||
setDefaultArrival(cfg.default_arrival_time ?? '15:00:00')
|
setDefaultArrival(cfg.default_arrival_time ?? '15:00:00')
|
||||||
setDefaultDeparture(cfg.default_departure_time ?? '10:00:00')
|
setDefaultDeparture(cfg.default_departure_time ?? '10:00:00')
|
||||||
|
|
@ -86,6 +89,21 @@ export default function Settings() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleAutomationToggle() {
|
||||||
|
const next = !automationEnabled
|
||||||
|
setAutomationSaving(true); setError(''); setMsg('')
|
||||||
|
try {
|
||||||
|
await updateConfig('automation_enabled', next)
|
||||||
|
setAutomationEnabled(next)
|
||||||
|
setMsg(next ? 'Automation resumed' : 'Automation paused — no setpoints will be sent to any device until resumed')
|
||||||
|
setTimeout(() => setMsg(''), 4000)
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Failed to update automation state')
|
||||||
|
} finally {
|
||||||
|
setAutomationSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveGeneralConfig() {
|
async function saveGeneralConfig() {
|
||||||
setConfigSaving(true); setError(''); setMsg('')
|
setConfigSaving(true); setError(''); setMsg('')
|
||||||
try {
|
try {
|
||||||
|
|
@ -130,6 +148,26 @@ export default function Settings() {
|
||||||
{error && <div className="error-banner">{error}</div>}
|
{error && <div className="error-banner">{error}</div>}
|
||||||
{msg && <div className="ok-banner">{msg}</div>}
|
{msg && <div className="ok-banner">{msg}</div>}
|
||||||
|
|
||||||
|
<div className="section-title">Automation</div>
|
||||||
|
<div className="card" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, marginBottom: 24 }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, color: automationEnabled ? 'var(--health-healthy)' : 'var(--danger)' }}>
|
||||||
|
{automationEnabled ? 'Automation is ON' : 'Automation is PAUSED'}
|
||||||
|
</div>
|
||||||
|
<div className="muted" style={{ fontSize: 13, marginTop: 2 }}>
|
||||||
|
Master switch for every zone — overrides each zone's own auto mode. While paused, the scheduler
|
||||||
|
skips its poll entirely: no NewBook fetch, no state tracking, no setpoints sent to any device.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={`btn ${automationEnabled ? '' : 'btn-primary'}`}
|
||||||
|
disabled={automationSaving}
|
||||||
|
onClick={handleAutomationToggle}
|
||||||
|
>
|
||||||
|
{automationSaving ? 'Saving…' : automationEnabled ? 'Pause Automation' : 'Resume Automation'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="section-title">MQTT Broker</div>
|
<div className="section-title">MQTT Broker</div>
|
||||||
<div className="card" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div className="card" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
{mqttConnected ? <Wifi size={16} strokeWidth={1.75} color="var(--health-healthy)" /> : <WifiOff size={16} strokeWidth={1.75} color="var(--danger)" />}
|
{mqttConnected ? <Wifi size={16} strokeWidth={1.75} color="var(--health-healthy)" /> : <WifiOff size={16} strokeWidth={1.75} color="var(--danger)" />}
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,7 @@ export interface NewbookSite {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
|
automation_enabled?: boolean
|
||||||
poll_interval_minutes: number
|
poll_interval_minutes: number
|
||||||
default_arrival_time: string
|
default_arrival_time: string
|
||||||
default_departure_time: string
|
default_departure_time: string
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue