Repository navigation
feat(octopus): octopus_intelligent_dynamic - confirm Intelligent slots only well inside a dispatch - #5248
Conversation
…s only well inside a dispatch Checking Octopus Intelligent slots against whether the car is charging (from #5229) was gated on metric_dynamic_load_adjust, which defaults Off, and predbat_config.json persists every setting, so almost no install had it on. A live test on a Wallbox found nothing was ever cancelled for exactly that reason. - New switch.predbat_octopus_intelligent_dynamic (expert mode, default On) gates it instead. It still only runs for an Intelligent car with evidence: a real car_charging_now entity, or the house load with the car inside the CT clamp. So existing installs with a sensor get it without changing a setting. metric_dynamic_load_adjust is now only the load estimate. The trust switch's Off mode needs the new switch, and warns when it is Off. Octopus schedules dispatches to the minute (the live one was 15:55-16:01:22, two entries), so the check now judges them that way: - "In a slot" uses the exact clock (now_utc_real plus skew), not minutes_now floored to 5 minutes, in the check and the poll alike. A dispatch ending at 12:37 is over at 12:37, not 12:40, and the car stopping because it ended is no longer read as "not charging". - Back-to-back slots are one dispatch, so a long dispatch split into half hours has no start band at each join. - A sensor's "not charging" only counts from 3 minutes into the dispatch, while the car and charger wake up (earliest cancel start + 5). An in-progress dispatch's start can be advanced to now each cycle, so the earliest start seen is kept, keyed on the dispatch's end. - A load reading is a 5 minute average and only counts when that whole window lies inside the dispatch. The grace is one more window on the 5 minute grid, so two low windows in a row cancel: start + 10 on the grid. - "Charging" from the sensor still counts anywhere in the dispatch. There is no band at the end. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A new "Checking Intelligent dispatches against the car" section under Octopus-led charging covers: - why the check exists (a dispatch the car does not use may be billed at the full rate); - how to set it up, with a charging-power sensor example; - how it decides (the sensor and load timings as a table); - what happens when the car is not charging, and what the plan and log show; - a worked example of a 15:55-16:01 dispatch; - troubleshooting for when nothing is ever cancelled, including that the "Dynamic load last period" line does not show the check is running. The switch bullets now point to that section instead of repeating it. The rate-strip log line now shares the "Octopus Intelligent:" prefix with the others. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ncellation Reproduces a live sequence on main. The car was cancelled at 16:42 against the planned 16:30-17:00 entry, then at 16:43 Octopus replaced it with a started entry 16:40:39-17:00. Judged on minutes_now floored to 16:40, that looked like leaving the slot, and the cancellation was dropped although car_charging_now never changed. The exact clock and the remembered dispatch start keep it; the test fails with the floored minute. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test_snapshot_time_formatting searched the whole snapshot for "1590"/"1860", the charge window's raw minute counts. The snapshot also carries the Predbat version with its git hash, so the test failed whenever CI's merge commit hash contained either string: PR #5248's run hashed to 3315908. It now checks only the planned-window lines, and pins that version string so the case is covered for good. Reverting format_window to raw minutes still fails it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| changed = True | ||
| if cancelled and self.dynamic_load_car_in_slot(car_n, self.minutes_now): | ||
| reason = "is in a charging slot but not charging, cancelling its slots" | ||
| if cancelled and self.dynamic_load_car_in_slot(car_n, minute): |
There was a problem hiding this comment.
Minor: for a cancelled car the merged dispatch is computed and the slot list sorted twice in this cycle - once inside dynamic_load_car_target (line 510) and again here via dynamic_load_car_in_slot - and dynamic_load_car_dispatch re-sorts the slots per car every 15 seconds in the poll. dynamic_load_car_in_slot (line 459) now has exactly one caller, this line. Reusing the dispatch tuple the target call already produced (or dropping the wrapper) removes the duplicate sort/merge.
There was a problem hiding this comment.
Reply from Claude Code, working with @springfall2008.
Fixed in 2989f8e. dynamic_load_car_in_slot() is removed, and its one caller uses dynamic_load_car_dispatch() directly. That call only runs when a car's state changes, to choose the log wording, so the extra merge doesn't happen on ordinary cycles. I left the poll's per-car sort as it is: it sorts a handful of one car's slots every 15 seconds, which isn't worth caching state for.
There was a problem hiding this comment.
Reply from Claude Code, working with @springfall2008.
Fixed in 2989f8e: dynamic_load_car_in_slot() is gone, and its one caller now calls dynamic_load_car_dispatch(car_n, minute) is not None directly. That call only runs when a car's cancellation state changes, to choose the log wording, so the extra sort happens rarely. The poll's per-car sort is over a car's own few slots every 15 seconds, so I've left it as is.
…onfig warnings - dynamic_load_car_in_slot() had a single caller, the log wording on a state change, so it now calls dynamic_load_car_dispatch() directly. - dynamic_load_car_check_config()'s docstring described only the cars list; it now covers all three warnings it logs and what it returns. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Follow-up to #5229 and #5245.
Problem
#5229 checks Octopus Intelligent slots against whether the car is actually charging, and cancels them if it isn't. Two things stopped it working in practice.
switch.predbat_metric_dynamic_load_adjust, which defaults Off.predbat_config.jsonsaves every setting, defaults included, so almost every install hasfalsestored, and changing the default would only reach new installs. A live test on a Wallbox showed exactly this: the car was in a dispatch at 0 W, and not onecar ...decision line was ever logged.minutes_now, rounded down to 5 minutes. So a dispatch ending at 12:37 still looked in progress until 12:40, and the car stopping because it ended counted as "not charging". There was also no allowance for the car and charger waking up at the start.Change
switch.predbat_octopus_intelligent_dynamic(expert mode, default On) gates slot confirmation. It still only runs for an Intelligent car with evidence: a realcar_charging_nowentity, or the house load with the car inside the CT clamp. So existing installs with a sensor get it without changing a setting.metric_dynamic_load_adjustis now only the load estimate.now_utc_realplus skew), in the check and the 15-second poll alike, so the two still agree. Zeroing slots and building rates keepminutes_now.startcan be advanced to now each cycle, so the earliest start seen is kept, keyed on the dispatch's end.minutes_now, and only counts when that whole window lies inside the dispatch. The grace is one more window, timed on the 5-minute grid rather than the exact clock, because load only arrives on cycles. So two low windows in a row cancel: start + 10 for a dispatch starting on the grid.Live regression
A second live test on main showed a cancellation being dropped although
car_charging_nownever changed:minutes_now, rounded down to 16:40:00, which is before 16:40:39. So the car appeared to have left the slot, and it resumed.Test 33b reproduces this: it passes here, and fails with the rounded clock. Here it's fixed twice over: by the exact clock, and by the dispatch start remembered against the unchanged 17:00 end.
Behaviour changes
car_charging_nowsensor now get slot confirmation without changing a setting.Tests
test_dynamic_load_car_not_charging(51 scenarios)._reset()now leaves dynamic load off, so every scenario shows the feature working without it. New cases:Deliberately broken versions of the start band, the merging, the remembered start, the exact clock, the load window, the load timing and the switch gate each make it fail.
./run_all --quick: all passed. pre-commit: clean.🤖 Generated with Claude Code