Owner decision 2026-09-29 (option A): maintenance_start_hour is read in local time, and the default moves from 10 to 19. Brought up to date with release/1.0 (0.15.0 + SAFETY-90) by a normal merge.
What changes
Zone: Home Assistant's time_zone (GET /core/api/config, 3 s budget) at startup; else the container's TZ; else UTC (logged as WARNING). Resolved once per start.
Every pack: at two inverters both packs get the same zone; arbitrate() is unchanged, so the pack that waits starts at the local hour on a later day.
DST (maintenance.in_start_slot(), UTC instants only): clocks forward and the hour does not exist → the first hour after the jump. Clocks back and the hour repeats → first pass only.
Default 19 in config.yaml and MAINT_OPTION_DEFAULTS; schema unchanged int(0,23).
Home screen shows the start at the instant the cycle uses (next_slot_start()).
Settings text: EN "Start hour (local time)" — "The hour on your local clock, 0 to 23, when a due cycle starts. 19 means 19:00." NL "Startuur (lokale tijd)" — "Het uur op je lokale klok, 0 tot 23, waarop een geplande cyclus begint. 19 is 19:00."
Release note draft (for the version bump; the version gate refuses an ## Unreleased block)
The maintenance start hour is now local time, and the default is 19:00 (MAINT-10). It used to be read as UTC. An existing install keeps its saved hour, and that hour now means local time: a saved 10 starts at 10:00, not 12:00 in summer. If you set an earlier hour to get an evening start (for example 17 for 19:00 in summer), set it to 19.
Live site: runs 17 as a stopgap for 19:00 summer; set it to 19 after deploy.
Tests (CI image, no tzdata: EU rule; plus zoneinfo Europe/Brussels with tzdata)
test_maint10.py: UTC identical to the old rule over a year; 19 → 17:00 UTC summer / 18:00 winter; every hour 0–23 on 29 Mar 2026, 28 Mar 2027 and 25 Oct 2026 gives one 60 min slot; one start per day with abort/deferral in the repeated hour; Home screen = slot start; zone order HA → TZ → UTC; default 19; real amain() wiring; real Controller identical for 11 UTC vs 13 Brussels at N=1 and N=2; N=2 two due packs across 24/25 Oct and 27/28 Mar start at the local hour on successive days, and a pack freed in the repeated hour does not start there.
Mutations (13, all killed; fresh copy each, CI image)
mutation
reds
M1 start decision back to the UTC hour
test_maint10 13
M2 slot ignores the zone
test_maint10 24, test_home 1
M3 repeated hour: both passes count
test_maint10 6
M4 missing hour: never starts
test_maint10 6
M13 missing hour: starts an hour late
test_maint10 6
M5 / M6 default left at 10 (mirror / config.yaml)
test_maint10, test_maintenance, test_defaults_pin 1 each
M7 amain does not pass the zone
test_maint10 4
M8 Controller drops the zone
test_maint10 5
M9 TZ before HA
test_maint10 5
M12 HA zone ignored
test_maint10 4
M11 N=2: only the first pack gets the zone
test_maint10 3
M10 Home screen shows the UTC hour
test_home 1
Full suite on 0b6b34f in the CI image with admin/goodwe at the pin: 64/64 files pass.
Owner decision 2026-09-29 (option A): `maintenance_start_hour` is read in **local time**, and the default moves from 10 to **19**. Brought up to date with release/1.0 (0.15.0 + SAFETY-90) by a normal merge.
## What changes
- **Zone**: Home Assistant's `time_zone` (`GET /core/api/config`, 3 s budget) at startup; else the container's `TZ`; else UTC (logged as WARNING). Resolved once per start.
- **Every pack**: at two inverters both packs get the same zone; `arbitrate()` is unchanged, so the pack that waits starts at the local hour on a later day.
- **DST** (`maintenance.in_start_slot()`, UTC instants only): clocks forward and the hour does not exist → the first hour after the jump. Clocks back and the hour repeats → first pass only.
- **Default** 19 in `config.yaml` and `MAINT_OPTION_DEFAULTS`; schema unchanged `int(0,23)`.
- **Home screen** shows the start at the instant the cycle uses (`next_slot_start()`).
- **Settings text**: EN "Start hour (local time)" — "The hour on your local clock, 0 to 23, when a due cycle starts. 19 means 19:00." NL "Startuur (lokale tijd)" — "Het uur op je lokale klok, 0 tot 23, waarop een geplande cyclus begint. 19 is 19:00."
- DOCS (Maintenance), DECISIONS/MAINT-10-local-start-hour.md.
## Release note draft (for the version bump; the version gate refuses an `## Unreleased` block)
> - **The maintenance start hour is now local time, and the default is 19:00** ([[MAINT-10]]). It used to be read as UTC. An existing install keeps its saved hour, and that hour now means local time: a saved 10 starts at 10:00, not 12:00 in summer. If you set an earlier hour to get an evening start (for example 17 for 19:00 in summer), set it to 19.
Live site: runs 17 as a stopgap for 19:00 summer; set it to 19 after deploy.
## Tests (CI image, no tzdata: EU rule; plus zoneinfo Europe/Brussels with tzdata)
`test_maint10.py`: UTC identical to the old rule over a year; 19 → 17:00 UTC summer / 18:00 winter; every hour 0–23 on 29 Mar 2026, **28 Mar 2027** and **25 Oct 2026** gives one 60 min slot; one start per day with abort/deferral in the repeated hour; Home screen = slot start; zone order HA → TZ → UTC; default 19; real `amain()` wiring; real Controller identical for 11 UTC vs 13 Brussels at **N=1 and N=2**; **N=2** two due packs across 24/25 Oct and 27/28 Mar start at the local hour on successive days, and a pack freed in the repeated hour does not start there.
## Mutations (13, all killed; fresh copy each, CI image)
| mutation | reds |
|---|---|
| M1 start decision back to the UTC hour | test_maint10 13 |
| M2 slot ignores the zone | test_maint10 24, test_home 1 |
| M3 repeated hour: both passes count | test_maint10 6 |
| M4 missing hour: never starts | test_maint10 6 |
| M13 missing hour: starts an hour late | test_maint10 6 |
| M5 / M6 default left at 10 (mirror / config.yaml) | test_maint10, test_maintenance, test_defaults_pin 1 each |
| M7 amain does not pass the zone | test_maint10 4 |
| M8 Controller drops the zone | test_maint10 5 |
| M9 TZ before HA | test_maint10 5 |
| M12 HA zone ignored | test_maint10 4 |
| M11 N=2: only the first pack gets the zone | test_maint10 3 |
| M10 Home screen shows the UTC hour | test_home 1 |
Full suite on 0b6b34f in the CI image with admin/goodwe at the pin: 64/64 files pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_019DBUfvndgHwaV339Fvd2gr
The start hour was compared with the UTC hour, so the shipped 10 started at
12:00 in a Belgian summer. It is now read in Home Assistant's time zone
(fallback: the container's TZ, then UTC, one log line says which), and the
default moves to 19. DST: a missing hour starts at the first hour after the
jump; a repeated hour starts only in its first pass. Stored options are not
rewritten: a saved 10 now means 10:00 local (DOCS, CHANGELOG).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_maint10.py part 10: at two inverters both packs get the site zone, the
real Controller is identical for 11 UTC and 13 Brussels, and two due packs
across 24/25 Oct 2026 and 27/28 Mar 2027 start at the local hour on
successive days; a pack freed in the repeated hour does not start there.
Part 3 adds 28 Mar 2027. DOCS: an existing install keeps its saved hour, now
read as local time; an hour set to make up for UTC goes back to 19.
DECISIONS: the SAFETY-83 gate is met; section on two inverters.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HA can take minutes to start (2026-10-05, 502 from the proxy). Then TZ holds,
which the Supervisor sets from HA's zone, so the hour stays right; only a TZ
of UTC there now logs a WARNING asking for a restart. A test pins the 3 s
budget on the zone request (a mutant without it survived).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F1. HA not up at add-on start (fixed in 1d28daf, partly refuted). The worry was that a 502 from the proxy at start (2026-10-05, HA took ~9 min) leaves the add-on on UTC for its whole life. On a Supervisor install it does not: HA pushes its zone to the Supervisor (core hassio/__init__.pypush_config → set_options(timezone=…)), the Supervisor stores it and sets TZ on the add-on container (docker/app.py, ENV_TIME: self.sys_timezone, checked in supervisor 2026.08.0). So the fallback is HA's own zone. What was left: if that TZ is UTC (the Supervisor has no zone), the start shifts and the log only said INFO "local time in UTC". Fix: that case now logs a WARNING: "Maintenance start hour uses UTC, because Home Assistant did not answer. Restart the add-on when it is up." No later re-resolve: the fallback already is HA's zone, so a warning is enough for the rare UTC case. DOCS + DECISIONS §1 say this. Tests: part 8, TZ=UTC → that WARNING; TZ=New_York → no warning.
F2. The 3 s budget on the zone request was not tested (fixed in 1d28daf). My mutant R1, which drops timeout= from ha_time_zone(), survived. A slow HA could then hold up control start. A check now pins timeout == DOCS_LINK_TIMEOUT_S (≤ 3 s).
Checked, no finding
tzdata:amd64-base:3.20 has tzdata, and apk python3 there gives ZoneInfo("Europe/Brussels") the right answer (2026-10-25 01:30Z → 02:30+01:00). (The "no tzdata in the shipped image" notes in price.py/static_tariff.py are stale, but they are out of scope.)
DST, my probes with real zoneinfo, every minute, through arbitrate(), with a 70-min cycle:
25 Oct 2026, hour 2: one slot, 00:00–01:00Z (first pass). Aborting every drain after 5 min does not restart in the second pass (cooldown would allow 01:35Z; the first-pass rule blocks it).
28 Mar 2027, hour 2: 03:00 CEST (01:00Z), once.
Hour 19: 17:00Z/18:00Z.
At N=2 the second pack starts the next local day at the same hour.
All 24 hours give exactly one 60-min slot on both days.
next_slot_start gives the same instants.
Every pack:Controller passes tz=site_tz to every pack's Maintenance, and homeview uses m.tz for each pack. Nothing else reads start_hour (grep).
Upgrade / 17: the only readers are wants_start and homeview, so a stored 17 means 17:00 local, with nothing special about it. The DOCS and release text is true and plain.
Default 19: set in config.yaml and MAINT_OPTION_DEFAULTS. The tests that pin it are test_maint10, test_maintenance and test_defaults_pin, plus probe §I. test_price_forecast pins 10 for its own scenario, which is fine.
Merge 08fb123:--remerge-diff shows only the two conflicts, both resolved keeping both sides. The diff against the base is just the 12 MAINT-10 files. No test was removed or weakened.
UX-08: the EN/NL label and description are short and plain.
## MAINT-10 review, round 1 (reviewer)
Reviewed 0b6b34f; fixes pushed as **1d28daf**.
### Findings
**F1. HA not up at add-on start (fixed in 1d28daf, partly refuted).** The worry was that a 502 from the proxy at start (2026-10-05, HA took ~9 min) leaves the add-on on UTC for its whole life. On a Supervisor install it does not: HA pushes its zone to the Supervisor (core `hassio/__init__.py` `push_config` → `set_options(timezone=…)`), the Supervisor stores it and sets `TZ` on the add-on container (`docker/app.py`, `ENV_TIME: self.sys_timezone`, checked in supervisor 2026.08.0). So the fallback is HA's own zone. What was left: if that `TZ` is `UTC` (the Supervisor has no zone), the start shifts and the log only said INFO "local time in UTC". **Fix:** that case now logs a WARNING: "Maintenance start hour uses UTC, because Home Assistant did not answer. Restart the add-on when it is up." No later re-resolve: the fallback already is HA's zone, so a warning is enough for the rare UTC case. DOCS + DECISIONS §1 say this. Tests: part 8, TZ=UTC → that WARNING; TZ=New_York → no warning.
**F2. The 3 s budget on the zone request was not tested (fixed in 1d28daf).** My mutant R1, which drops `timeout=` from `ha_time_zone()`, survived. A slow HA could then hold up control start. A check now pins `timeout == DOCS_LINK_TIMEOUT_S` (≤ 3 s).
### Checked, no finding
- **tzdata:** `amd64-base:3.20` has tzdata, and apk `python3` there gives `ZoneInfo("Europe/Brussels")` the right answer (2026-10-25 01:30Z → 02:30+01:00). (The "no tzdata in the shipped image" notes in price.py/static_tariff.py are stale, but they are out of scope.)
- **DST, my probes with real zoneinfo**, every minute, through `arbitrate()`, with a 70-min cycle:
- 25 Oct 2026, hour 2: one slot, 00:00–01:00Z (first pass). Aborting every drain after 5 min does not restart in the second pass (cooldown would allow 01:35Z; the first-pass rule blocks it).
- 28 Mar 2027, hour 2: 03:00 CEST (01:00Z), once.
- Hour 19: 17:00Z/18:00Z.
- At N=2 the second pack starts the next local day at the same hour.
- All 24 hours give exactly one 60-min slot on both days.
- `next_slot_start` gives the same instants.
- **Every pack:** `Controller` passes `tz=site_tz` to every pack's `Maintenance`, and `homeview` uses `m.tz` for each pack. Nothing else reads `start_hour` (grep).
- **Upgrade / 17:** the only readers are `wants_start` and `homeview`, so a stored 17 means 17:00 local, with nothing special about it. The DOCS and release text is true and plain.
- **Default 19:** set in config.yaml and `MAINT_OPTION_DEFAULTS`. The tests that pin it are test_maint10, test_maintenance and test_defaults_pin, plus probe §I. test_price_forecast pins 10 for its own scenario, which is fine.
- **Merge 08fb123:** `--remerge-diff` shows only the two conflicts, both resolved keeping both sides. The diff against the base is just the 12 MAINT-10 files. No test was removed or weakened.
- **UX-08:** the EN/NL label and description are short and plain.
### Mutations (mine, on 1d28daf content, CI image)
| | mutant | result |
|---|---|---|
| R1 | zone request without time budget | killed (after F2; survived before) |
| R2 | missing hour: `>=`→`>` (slot 1 min late) | killed, test_maint10 7 |
| R3 | `next_slot_start` no look-back | killed, test_maint10 1 |
| R4 | repeated hour: date test inverted | killed, test_maint10 6 |
| R5 | amain drops the TZ fallback | killed, test_maint10 3 |
| R6 | the new UTC warning dropped | killed, test_maint10 1 |
| R7 | zone-less `Maintenance` defaults to EU local | killed, 4 files |
Merging once `checks / unit` is green on 1d28daf.
admin
merged commit 1bf6b7f9a7 into release/1.02026-10-05 23:56:19 +02:00
CI checks / unit (pull_request) passed on 1d28daf (run 1186, a real 31-minute run). Local run in goodwe-addon-checks:1 with admin/goodwe at the pin: 64/64 files pass. Merged into release/1.0 as 1bf6b7f, pinned to head_commit_id 1d28daf; the merge tree is the same as the head tree. Both review findings (F1, F2) are fixed. Not tagged and not deployed.
For the rig test: the startup log line, the 19:00 local start on the Home screen and in control, and the stored-hour upgrade (see the review report).
## Review round 1 closed: merged
CI `checks / unit (pull_request)` passed on 1d28daf (run 1186, a real 31-minute run). Local run in `goodwe-addon-checks:1` with admin/goodwe at the pin: 64/64 files pass. Merged into `release/1.0` as **1bf6b7f**, pinned to `head_commit_id` 1d28daf; the merge tree is the same as the head tree. Both review findings (F1, F2) are fixed. Not tagged and not deployed.
For the rig test: the startup log line, the 19:00 local start on the Home screen and in control, and the stored-hour upgrade (see the review report).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Owner decision 2026-09-29 (option A):
maintenance_start_houris read in local time, and the default moves from 10 to 19. Brought up to date with release/1.0 (0.15.0 + SAFETY-90) by a normal merge.What changes
time_zone(GET /core/api/config, 3 s budget) at startup; else the container'sTZ; else UTC (logged as WARNING). Resolved once per start.arbitrate()is unchanged, so the pack that waits starts at the local hour on a later day.maintenance.in_start_slot(), UTC instants only): clocks forward and the hour does not exist → the first hour after the jump. Clocks back and the hour repeats → first pass only.config.yamlandMAINT_OPTION_DEFAULTS; schema unchangedint(0,23).next_slot_start()).Release note draft (for the version bump; the version gate refuses an
## Unreleasedblock)Live site: runs 17 as a stopgap for 19:00 summer; set it to 19 after deploy.
Tests (CI image, no tzdata: EU rule; plus zoneinfo Europe/Brussels with tzdata)
test_maint10.py: UTC identical to the old rule over a year; 19 → 17:00 UTC summer / 18:00 winter; every hour 0–23 on 29 Mar 2026, 28 Mar 2027 and 25 Oct 2026 gives one 60 min slot; one start per day with abort/deferral in the repeated hour; Home screen = slot start; zone order HA → TZ → UTC; default 19; realamain()wiring; real Controller identical for 11 UTC vs 13 Brussels at N=1 and N=2; N=2 two due packs across 24/25 Oct and 27/28 Mar start at the local hour on successive days, and a pack freed in the repeated hour does not start there.Mutations (13, all killed; fresh copy each, CI image)
Full suite on
0b6b34fin the CI image with admin/goodwe at the pin: 64/64 files pass.🤖 Generated with Claude Code
https://claude.ai/code/session_019DBUfvndgHwaV339Fvd2gr
MAINT-10 review, round 1 (reviewer)
Reviewed 0b6b34f; fixes pushed as
1d28daf.Findings
F1. HA not up at add-on start (fixed in
1d28daf, partly refuted). The worry was that a 502 from the proxy at start (2026-10-05, HA took ~9 min) leaves the add-on on UTC for its whole life. On a Supervisor install it does not: HA pushes its zone to the Supervisor (corehassio/__init__.pypush_config→set_options(timezone=…)), the Supervisor stores it and setsTZon the add-on container (docker/app.py,ENV_TIME: self.sys_timezone, checked in supervisor 2026.08.0). So the fallback is HA's own zone. What was left: if thatTZisUTC(the Supervisor has no zone), the start shifts and the log only said INFO "local time in UTC". Fix: that case now logs a WARNING: "Maintenance start hour uses UTC, because Home Assistant did not answer. Restart the add-on when it is up." No later re-resolve: the fallback already is HA's zone, so a warning is enough for the rare UTC case. DOCS + DECISIONS §1 say this. Tests: part 8, TZ=UTC → that WARNING; TZ=New_York → no warning.F2. The 3 s budget on the zone request was not tested (fixed in
1d28daf). My mutant R1, which dropstimeout=fromha_time_zone(), survived. A slow HA could then hold up control start. A check now pinstimeout == DOCS_LINK_TIMEOUT_S(≤ 3 s).Checked, no finding
amd64-base:3.20has tzdata, and apkpython3there givesZoneInfo("Europe/Brussels")the right answer (2026-10-25 01:30Z → 02:30+01:00). (The "no tzdata in the shipped image" notes in price.py/static_tariff.py are stale, but they are out of scope.)arbitrate(), with a 70-min cycle:next_slot_startgives the same instants.Controllerpassestz=site_tzto every pack'sMaintenance, andhomeviewusesm.tzfor each pack. Nothing else readsstart_hour(grep).wants_startandhomeview, so a stored 17 means 17:00 local, with nothing special about it. The DOCS and release text is true and plain.MAINT_OPTION_DEFAULTS. The tests that pin it are test_maint10, test_maintenance and test_defaults_pin, plus probe §I. test_price_forecast pins 10 for its own scenario, which is fine.08fb123:--remerge-diffshows only the two conflicts, both resolved keeping both sides. The diff against the base is just the 12 MAINT-10 files. No test was removed or weakened.Mutations (mine, on
1d28dafcontent, CI image)>=→>(slot 1 min late)next_slot_startno look-backMaintenancedefaults to EU localMerging once
checks / unitis green on1d28daf.Review round 1 closed: merged
CI
checks / unit (pull_request)passed on1d28daf(run 1186, a real 31-minute run). Local run ingoodwe-addon-checks:1with admin/goodwe at the pin: 64/64 files pass. Merged intorelease/1.0as1bf6b7f, pinned tohead_commit_id1d28daf; the merge tree is the same as the head tree. Both review findings (F1, F2) are fixed. Not tagged and not deployed.For the rig test: the startup log line, the 19:00 local start on the Home screen and in control, and the stored-hour upgrade (see the review report).