Harden AC rules with workday+weather fallbacks (v2)
Replace the three live AC automations in place (same ids -> same entity_ids, continuous history/traces): * pre-cool & peak-setback: the workday check moves out of a blocking `condition` into template logic that falls back to a plain Mon-Fri test when binary_sensor.workday_sensor is unavailable/unknown. Previously a dead sensor silently skipped the run. * pre-cool: weather.get_forecasts now uses continue_on_error, and a `fc is defined and is mapping` guard falls back to an assumed 88F high (->70F regular pre-cool) when the forecast hard-errors or returns nothing. * peak-ends: unchanged behavior (runs daily), documented only. Add AUTOMATIONS.md (identity model, per-rule spec, failure-mode table, deploy steps) and deploy.py (pushes automations/*.yaml to the HA config API by id). Fallbacks verified empirically against the live automations via execution traces before this commit. Also removed throwaway server-side debug automations (three TEST spoofs + the ac_flap_1758_1820 experiment). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cfb3520ef7
commit
1e3c1001d0
4 changed files with 203 additions and 95 deletions
88
AUTOMATIONS.md
Normal file
88
AUTOMATIONS.md
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
# Home Assistant automations
|
||||||
|
|
||||||
|
Source of truth for the AC peak-shaving automations on the HA server
|
||||||
|
(`192.168.128.3:8123`). This repo is authoritative; HA is a **deploy target**
|
||||||
|
reached only through its REST config API — HA never sees these files directly.
|
||||||
|
|
||||||
|
## Identity model (how a file becomes a live automation)
|
||||||
|
|
||||||
|
```
|
||||||
|
file (git) Home Assistant
|
||||||
|
automations/ac_peak_setback.yaml ──deploy.py──▶ POST /api/config/automation/
|
||||||
|
id: ac_peak_setback (by id) config/ac_peak_setback
|
||||||
|
alias: "AC - Peak setback to 82" │
|
||||||
|
... ▼ registers entity
|
||||||
|
automation.ac_peak_setback_to_82
|
||||||
|
```
|
||||||
|
|
||||||
|
| Identifier | Example | Set by | Used by | Change it? |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **file name** | `ac_peak_setback.yaml` | you | git only (HA never sees it) | freely |
|
||||||
|
| **`id`** | `ac_peak_setback` | you (in YAML) | config API URL, traces, `automation.trigger` | **never** (primary key) |
|
||||||
|
| **`alias`** | `AC - Peak setback to 82` | you (in YAML) | UI name; seeds `entity_id` at first create | rarely |
|
||||||
|
| **`entity_id`** | `automation.ac_peak_setback_to_82` | HA, from `slugify(alias)` | dashboards, other automations, scripts | sticky once registered |
|
||||||
|
|
||||||
|
Key point: **`entity_id` derives from `alias`, not `id`.** Reference automations
|
||||||
|
elsewhere in HA by their `entity_id`.
|
||||||
|
|
||||||
|
## Deploying
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source ~/.bashrc # provides HA_API_KEY (long-lived token)
|
||||||
|
python3 deploy.py --check # parse only, no writes
|
||||||
|
python3 deploy.py # replace all three in place (same ids)
|
||||||
|
python3 deploy.py ac_peak_ends # deploy just one by id
|
||||||
|
```
|
||||||
|
|
||||||
|
`deploy.py` POSTs each file to `/api/config/automation/config/<id>`. Same `id`
|
||||||
|
= in-place replacement, so `entity_id`, run history, and traces stay continuous.
|
||||||
|
|
||||||
|
## The rules
|
||||||
|
|
||||||
|
All act on `climate.t6_pro_z_wave_programmable_thermostat_with_smartstart`.
|
||||||
|
The "peak" window is **14:00–19:00 (2–7 PM) local (America/New_York)**.
|
||||||
|
|
||||||
|
### 1. `ac_precool_before_peak` — Pre-cool before peak
|
||||||
|
- **12:00**, work days. Fetches today's daily forecast high and pre-cools:
|
||||||
|
| forecast high | setpoint |
|
||||||
|
|---|---|
|
||||||
|
| ≥ 90°F | 68 |
|
||||||
|
| ≥ 85°F | 70 |
|
||||||
|
| ≥ 80°F | 71 |
|
||||||
|
| < 80°F | *(no pre-cool)* |
|
||||||
|
- **Fallbacks:** workday sensor broken → Mon–Fri; forecast broken → assume 88°F
|
||||||
|
high → 70°F (`continue_on_error` covers a hard error, the `fc is defined and is
|
||||||
|
mapping` guard covers an empty/None response).
|
||||||
|
|
||||||
|
### 2. `ac_peak_setback` — Peak setback to 82
|
||||||
|
- **14:00**, work days. Sets cool / **82°F** to coast through the peak.
|
||||||
|
- **Fallback:** workday sensor broken → Mon–Fri.
|
||||||
|
|
||||||
|
### 3. `ac_peak_ends` — Peak ends, return to 74
|
||||||
|
- **19:00**, **every day** (intentionally not workday-gated). Sets **74°F**.
|
||||||
|
- No external dependencies, so nothing to fall back on.
|
||||||
|
|
||||||
|
## Failure-mode design
|
||||||
|
|
||||||
|
Two independent dependencies, each handled so a failure keeps sensible defaults
|
||||||
|
instead of skipping:
|
||||||
|
|
||||||
|
| Failure | Symptom | Handled by | Default taken |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Workday/holiday sensor unavailable | `states()` ≠ `on`/`off` | `{% if wd in ['on','off'] %}…{% else %} now().weekday()<5` | Mon–Fri |
|
||||||
|
| Weather service raises | action would abort | `continue_on_error: true` | run continues |
|
||||||
|
| Weather returns empty/None | `fc` undefined / not a mapping | `{% if fc is defined and fc is mapping %}…{% else %} 88` | regular pre-cool (88→70) |
|
||||||
|
|
||||||
|
Verified empirically on 2026-07-09 by forcing each failure against the live
|
||||||
|
automations and reading their execution traces (workday → `unavailable` still
|
||||||
|
ran the Mon–Fri branch; missing weather entity → `today_high=88`, setpoint 70,
|
||||||
|
`script_execution: finished`).
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
- **2026-07-09 — v1 baseline**: imported the three live automations as-is.
|
||||||
|
- **2026-07-09 — v2**: added workday + weather fallbacks to pre-cool and
|
||||||
|
setback (a broken sensor/forecast no longer skips the run). Peak-ends
|
||||||
|
unchanged. Removed throwaway debug automations from the server: the three
|
||||||
|
`TEST - … (spoof, every 1 min)` rules and the `ac_flap_1758_1820`
|
||||||
|
(85/90 flap) experiment.
|
||||||
|
|
@ -1,15 +1,29 @@
|
||||||
# ac_peak_ends — imported live baseline (v1) on 2026-07-09
|
# ac_peak_ends — end of the utility peak, return the house to comfort
|
||||||
|
#
|
||||||
|
# id / entity : ac_peak_ends -> automation.ac_peak_ends_return_to_74
|
||||||
|
# trigger : 19:00 local (America/New_York), EVERY day
|
||||||
|
# purpose : Return the thermostat to 74F after the peak window.
|
||||||
|
#
|
||||||
|
# Runs daily on purpose (no workday gate): you always want comfort back in the
|
||||||
|
# evening, weekend or holiday included. It has no external dependencies (no
|
||||||
|
# weather, no workday sensor), so there is nothing to fall back on — it is a
|
||||||
|
# plain, robust setpoint write.
|
||||||
|
#
|
||||||
|
# v2 (2026-07-09): no behavior change; documented + version-controlled.
|
||||||
id: ac_peak_ends
|
id: ac_peak_ends
|
||||||
alias: AC - Peak ends, return to 74
|
alias: AC - Peak ends, return to 74
|
||||||
description: ''
|
description: >-
|
||||||
|
v2 (2026-07-09). At 7pm every day, returns the thermostat to 74F after the
|
||||||
|
peak window. Intentionally runs daily (no workday gate) and has no external
|
||||||
|
dependencies. Managed in git; see AUTOMATIONS.md.
|
||||||
|
mode: single
|
||||||
triggers:
|
triggers:
|
||||||
- trigger: time
|
- trigger: time
|
||||||
at: '19:00:00'
|
at: "19:00:00"
|
||||||
conditions: []
|
conditions: []
|
||||||
actions:
|
actions:
|
||||||
- action: climate.set_temperature
|
- action: climate.set_temperature
|
||||||
target:
|
target:
|
||||||
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
||||||
data:
|
data:
|
||||||
temperature: 74
|
temperature: 74
|
||||||
mode: single
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,41 @@
|
||||||
# ac_peak_setback — imported live baseline (v1) on 2026-07-09
|
# ac_peak_setback — let the house drift during the utility peak window
|
||||||
|
#
|
||||||
|
# id / entity : ac_peak_setback -> automation.ac_peak_setback_to_82
|
||||||
|
# trigger : 14:00 local (America/New_York)
|
||||||
|
# purpose : On work days, set cool 82F to coast through the 14:00-19:00 peak.
|
||||||
|
#
|
||||||
|
# Graceful degradation (v2):
|
||||||
|
# * workday sensor unavailable/unknown -> fall back to a plain Mon-Fri check
|
||||||
|
# (a broken holiday sensor must not silently cancel the setback).
|
||||||
|
# The original used a blocking `condition` on the workday sensor, which would
|
||||||
|
# skip the run whenever the sensor was unavailable; that is the bug this fixes.
|
||||||
|
#
|
||||||
|
# v2 (2026-07-09): workday fallback added. See AUTOMATIONS.md.
|
||||||
id: ac_peak_setback
|
id: ac_peak_setback
|
||||||
alias: AC - Peak setback to 82
|
alias: AC - Peak setback to 82
|
||||||
description: ''
|
description: >-
|
||||||
triggers:
|
v2 (2026-07-09). At 2pm on work days, sets the thermostat to cool/82F to coast
|
||||||
- trigger: time
|
through the 2-7pm peak. Falls back to Mon-Fri if the workday sensor fails.
|
||||||
at: '14:00:00'
|
Managed in git; see AUTOMATIONS.md.
|
||||||
conditions:
|
|
||||||
- condition: state
|
|
||||||
entity_id: binary_sensor.workday_sensor
|
|
||||||
state: 'on'
|
|
||||||
actions:
|
|
||||||
- action: climate.set_hvac_mode
|
|
||||||
target:
|
|
||||||
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
|
||||||
data:
|
|
||||||
hvac_mode: cool
|
|
||||||
- action: climate.set_temperature
|
|
||||||
target:
|
|
||||||
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
|
||||||
data:
|
|
||||||
temperature: 82
|
|
||||||
mode: single
|
mode: single
|
||||||
|
triggers:
|
||||||
|
- trigger: time
|
||||||
|
at: "14:00:00"
|
||||||
|
conditions: []
|
||||||
|
actions:
|
||||||
|
- variables:
|
||||||
|
is_workday: "{% set wd = states('binary_sensor.workday_sensor') %}{% if wd in ['on','off'] %}{{ wd == 'on' }}{% else %}{{ now().weekday() < 5 }}{% endif %}"
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ is_workday }}"
|
||||||
|
then:
|
||||||
|
- action: climate.set_hvac_mode
|
||||||
|
target:
|
||||||
|
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
||||||
|
data:
|
||||||
|
hvac_mode: cool
|
||||||
|
- action: climate.set_temperature
|
||||||
|
target:
|
||||||
|
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
||||||
|
data:
|
||||||
|
temperature: 82
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,56 @@
|
||||||
# ac_precool_before_peak — imported live baseline (v1) on 2026-07-09
|
# ac_precool_before_peak — pre-cool the house before the utility peak window
|
||||||
|
#
|
||||||
|
# id / entity : ac_precool_before_peak -> automation.ac_pre_cool_before_peak
|
||||||
|
# trigger : 12:00 local (America/New_York)
|
||||||
|
# purpose : On work days, pre-cool before the 14:00-19:00 peak, harder when
|
||||||
|
# the forecast high is hotter (>=90 -> 68, >=85 -> 70, >=80 -> 71,
|
||||||
|
# below 80 -> no pre-cool).
|
||||||
|
#
|
||||||
|
# Graceful degradation (v2) — a broken dependency must NOT skip pre-cooling:
|
||||||
|
# * workday sensor unavailable/unknown -> fall back to a plain Mon-Fri check.
|
||||||
|
# * weather forecast fails or returns nothing -> assume a regular hot day
|
||||||
|
# (high 88F -> 70F). weather.get_forecasts runs with continue_on_error so a
|
||||||
|
# hard service error can't abort the run; the `fc is defined and is mapping`
|
||||||
|
# guard catches the soft-failure (empty/None) case.
|
||||||
|
#
|
||||||
|
# NOTE: templates are kept on single lines on purpose — YAML folded scalars can
|
||||||
|
# inject stray whitespace/newlines into Jinja. Readability lives in this header.
|
||||||
|
#
|
||||||
|
# v2 (2026-07-09): added workday + weather fallbacks. See AUTOMATIONS.md.
|
||||||
id: ac_precool_before_peak
|
id: ac_precool_before_peak
|
||||||
alias: AC - Pre-cool before peak
|
alias: AC - Pre-cool before peak
|
||||||
description: ''
|
description: >-
|
||||||
triggers:
|
v2 (2026-07-09). Pre-cools before the 2-7pm peak on work days. Falls back to
|
||||||
- trigger: time
|
Mon-Fri if the workday sensor fails, and to a regular pre-cool (assume 88F high
|
||||||
at: '12:00:00'
|
-> 70F) if the weather forecast fails. Managed in git; see AUTOMATIONS.md.
|
||||||
conditions:
|
|
||||||
- condition: state
|
|
||||||
entity_id: binary_sensor.workday_sensor
|
|
||||||
state: 'on'
|
|
||||||
actions:
|
|
||||||
- action: weather.get_forecasts
|
|
||||||
target:
|
|
||||||
entity_id: weather.forecast_4315aspenhill
|
|
||||||
data:
|
|
||||||
type: daily
|
|
||||||
response_variable: fc
|
|
||||||
- variables:
|
|
||||||
today_high: "{% set today = now().date() %} {% set entries = fc['weather.forecast_4315aspenhill'].forecast\n\
|
|
||||||
\ | selectattr('datetime')\n | list %}\n{% set match = entries\n | selectattr('datetime',\
|
|
||||||
\ 'search', today | string)\n | list %}\n{{ (match[0].temperature if match\
|
|
||||||
\ else entries[0].temperature) | float(0) }}\n"
|
|
||||||
- choose:
|
|
||||||
- conditions:
|
|
||||||
- condition: template
|
|
||||||
value_template: '{{ today_high >= 90 }}'
|
|
||||||
sequence:
|
|
||||||
- action: climate.set_hvac_mode
|
|
||||||
target:
|
|
||||||
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
|
||||||
data:
|
|
||||||
hvac_mode: cool
|
|
||||||
- action: climate.set_temperature
|
|
||||||
target:
|
|
||||||
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
|
||||||
data:
|
|
||||||
temperature: 68
|
|
||||||
- conditions:
|
|
||||||
- condition: template
|
|
||||||
value_template: '{{ today_high >= 85 }}'
|
|
||||||
sequence:
|
|
||||||
- action: climate.set_hvac_mode
|
|
||||||
target:
|
|
||||||
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
|
||||||
data:
|
|
||||||
hvac_mode: cool
|
|
||||||
- action: climate.set_temperature
|
|
||||||
target:
|
|
||||||
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
|
||||||
data:
|
|
||||||
temperature: 70
|
|
||||||
- conditions:
|
|
||||||
- condition: template
|
|
||||||
value_template: '{{ today_high >= 80 }}'
|
|
||||||
sequence:
|
|
||||||
- action: climate.set_hvac_mode
|
|
||||||
target:
|
|
||||||
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
|
||||||
data:
|
|
||||||
hvac_mode: cool
|
|
||||||
- action: climate.set_temperature
|
|
||||||
target:
|
|
||||||
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
|
||||||
data:
|
|
||||||
temperature: 71
|
|
||||||
default: []
|
|
||||||
mode: single
|
mode: single
|
||||||
|
triggers:
|
||||||
|
- trigger: time
|
||||||
|
at: "12:00:00"
|
||||||
|
conditions: []
|
||||||
|
actions:
|
||||||
|
- action: weather.get_forecasts
|
||||||
|
continue_on_error: true
|
||||||
|
target:
|
||||||
|
entity_id: weather.forecast_4315aspenhill
|
||||||
|
data:
|
||||||
|
type: daily
|
||||||
|
response_variable: fc
|
||||||
|
- variables:
|
||||||
|
is_workday: "{% set wd = states('binary_sensor.workday_sensor') %}{% if wd in ['on','off'] %}{{ wd == 'on' }}{% else %}{{ now().weekday() < 5 }}{% endif %}"
|
||||||
|
today_high: "{% if fc is defined and fc is mapping and 'weather.forecast_4315aspenhill' in fc and fc['weather.forecast_4315aspenhill'].forecast | default([]) | length > 0 %}{% set today = now().date() %}{% set entries = fc['weather.forecast_4315aspenhill'].forecast | selectattr('datetime') | list %}{% set match = entries | selectattr('datetime','search', today | string) | list %}{{ (match[0].temperature if match else entries[0].temperature) | float(88) }}{% else %}88{% endif %}"
|
||||||
|
setpoint: "{% set h = today_high | float(88) %}{% if h >= 90 %}68{% elif h >= 85 %}70{% elif h >= 80 %}71{% else %}none{% endif %}"
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ is_workday and setpoint != 'none' }}"
|
||||||
|
then:
|
||||||
|
- action: climate.set_hvac_mode
|
||||||
|
target:
|
||||||
|
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
||||||
|
data:
|
||||||
|
hvac_mode: cool
|
||||||
|
- action: climate.set_temperature
|
||||||
|
target:
|
||||||
|
entity_id: climate.t6_pro_z_wave_programmable_thermostat_with_smartstart
|
||||||
|
data:
|
||||||
|
temperature: "{{ setpoint | int }}"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue