Pepco MD TOU-P has different on-peak windows by season: summer (Jun-Sep) 14:00-19:00, winter (Oct-May) 06:00-09:00 and 17:00-21:00. Heat is gas, so winter electric peak logic is cooling-only. * pre-cool: bounded to summer (Jun-Sep); winter has no pre-cool. * peak-setback (hold) and peak-ends (return): unified across seasons via a season self-select (month -> season; triggers carry id: summer/winter and act only when trigger.id == season) plus a params map for setpoints. Summer forces cool + 82 / returns 74 daily; winter sets 82 / returns 74 ONLY when already in cool mode, so it never disturbs the gas furnace. * new ac_winter_morning_kill: 06:00 winter work days, if in cool mode set hvac_mode off to dodge the 6-9am winter peak. No auto-resume yet (planned). Decision logic verified as a truth table via the template engine across summer/winter x cooling x workday before commit. Deployed in place (existing ids unchanged); new id ac_winter_morning_kill added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.4 KiB
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.
Utility rate context (Pepco MD, TOU-P, eff. 2026-02-01)
| Season (rate) | Months | On-peak window(s), weekdays only | On-peak | Off-peak |
|---|---|---|---|---|
| Summer | Jun–Sep | 14:00–19:00 (2–7 PM) | $0.42 | $0.17 |
| Winter | Oct–May | 06:00–09:00 and 17:00–21:00 | $0.32 | $0.13 |
Weekends and federal holidays are always off-peak — handled by
binary_sensor.workday_sensor (with a Mon–Fri fallback). Heating is gas
(not on this electric peak logic), so the winter rules are cooling-only.
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 |
entity_id derives from alias, not id. Reference automations elsewhere in
HA by their entity_id.
Deploying
source ~/.bashrc # provides HA_API_KEY (long-lived token)
python3 deploy.py --check # parse only, no writes
python3 deploy.py # deploy every automations/*.yaml (in place, by id)
python3 deploy.py ac_peak_ends # deploy just one by id
Parameterization (the "params map" model)
Rather than duplicate summer vs winter rules, each phase is one self-selecting automation:
- A
variables:block computesseasonfrom the month (summer= {6,7,8,9}, elsewinter). - Time triggers carry
id: summer/id: winter; the body only acts whentrigger.id == season, so the off-season time is a harmless no-op. - Setpoints come from a
paramsmap ({'summer': {'hold':82,'ret':74}, 'winter': {'hold':82,'ret':74}}) so the two seasons can diverge later by editing one line. (Both are equal today.) The map is repeated inac_peak_setbackandac_peak_ends; keep them in sync.
The rules
All act on climate.t6_pro_z_wave_programmable_thermostat_with_smartstart.
1. ac_precool_before_peak — Pre-cool before peak (summer only)
- 12:00, summer work days. Fetches today's forecast high and pre-cools: ≥90→68, ≥85→70, ≥80→71, <80→no pre-cool.
- Winter has no pre-cool (marginal for an evening peak).
- Fallbacks: workday sensor broken → Mon–Fri; forecast broken → assume 88°F
→ 70°F (
continue_on_error+fc is defined and is mappingguard).
2. ac_peak_setback — Peak setback to 82 (both seasons)
- 14:00 (summer) / 17:00 (winter), work days.
- Summer: forces
cooland sets 82. - Winter: cooling-only — sets 82 only if already in
coolmode (never forces mode, never touches gas heat).
3. ac_peak_ends — Peak ends, return to 74 (both seasons)
- 19:00 (summer) / 21:00 (winter). Sets 74.
- Summer returns daily; winter returns only if in
coolmode.
4. ac_winter_morning_kill — Winter morning peak kill (new in v3)
- 06:00, winter work days. If in
coolmode, setshvac_mode: offto dodge the 6–9 AM winter peak. No auto-resume yet (add later). Cooling-only.
Failure-mode design
| 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) |
| Wrong-season trigger fires | trigger.id != season |
applies = trigger.id == season guard |
no-op |
| Winter, not cooling (e.g. gas heat on) | is_state(climate,'cool') false |
cooling-only guard | no-op (furnace safe) |
Fallback and season/cooling logic verified via HA's template engine and execution traces before each deploy.
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;
removed throwaway debug automations (three
TESTspoofs +ac_flap_1758_1820). - 2026-07-09 — v3: TOU winter support. Pre-cool bounded to summer (Jun–Sep).
ac_peak_setbackandac_peak_endsunified across seasons via a season self-select +paramsmap (summer 14:00/19:00, winter 17:00/21:00), winter cooling-only. Addedac_winter_morning_kill(6 AM winter, cooling→off, no resume yet).