Repository navigation
Merge upstream springfall2008/batpred (v8.53.0-equivalent) into main - #52
Merged
Merged
Conversation
…nload Chasing a user through the per-snapshot picker to find the right moment is exactly the back-and-forth this buffer was meant to avoid. One link now downloads every retained snapshot as a gzip tarball; the plan page's own History/Yesterday view (a separate consumer of the same snapshot index) is untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…position steps_back is the snapshot's current position in the retention ring, not a property of the snapshot itself - it shifts as newer captures push it back. Baking it into the filename meant the same real capture got a different name in every archive downloaded after it, making archives from different times impossible to merge/deduplicate by filename. snapshot_id (its capture timestamp) is already unique and stable; that's all the filename needs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tention window Two captures landing in the same calendar minute (a routine capture and a close-by force-capture) used to both stay in the ring, reaching the UI as two identically-named chart series and breaking "Deselect all", which keys off series name. capture_snapshot() now discards the older of any same-minute pair, keeping the newer. Also add an optional max_age prune so a burst of close-together captures can't leave something far older than the buffer's intended window sitting there just because the count cap alone hasn't caught up to it - wired through from predbat.py as interval_hours * count. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ded raw disk writes Per @springfall2008's review on springfall2008#4438: "debug_enable still causes unbounded disk growth - the original problem isn't actually fixed yet." The rolling debug-history buffer (springfall2008#4417) shipped alongside the original raw create_debug_yaml() disk write, not in place of it, so leaving the switch on still wrote an uncapped predbat_debug_HH_MM_SS.yaml every ~5-minute cycle. Rather than remove the raw write outright: debug_enable also gates verbose logging and the C++ kernel bypass (the more accurate but far slower prediction path, see springfall2008#4453's investigation), both genuinely useful for watching a live issue develop cycle to cycle - finer-grained than the history buffer's coarsest 1-hour interval floor (debug_history_interval's min is 1, unit is hours). Removing the write entirely would be a real capability loss for exactly the kind of live debugging this session has been doing. Instead, track when debug_enable was turned on (debug_enable_started) and auto-disable it (expose_config, so it's visible in HA too) after DEBUG_ENABLE_MAX_HOURS (2h). Bounds both the disk growth and the standing performance cost of leaving the slow path on by accident, while preserving full-resolution capture for someone actively watching a problem. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…imum raised to 1 Per @springfall2008's review on springfall2008#4438: "give the rotating buffer its own on/off switch, rather than overloading debug_history_count == 0" - inconsistent with how the codebase normally does on/off (debug_enable and debug_history_force_capture are both real switch CONFIG_ITEMS already). Defaults on, preserving existing behaviour for anyone already relying on debug_history_count=15 as "on". debug_history_force_capture still fires regardless of this switch, matching the existing "explicit capture now request is a different intent to the rolling background history" design. Also raises debug_history_count's config-schema minimum from 0 to 1 - with a dedicated switch, count=0 no longer needs to double as an off-switch, so removing it as a valid value avoids "switch is on but count is 0" becoming a second, silently-conflicting way to end up disabled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…on genuine success Per @springfall2008's review on springfall2008#4438 (items 1-3): 1. debug_history_last_capture was reset unconditionally after the try/except, so a failed capture (an exception, caught and logged as a Warning) was silently treated as if it had succeeded - deferring the next routine retry a full debug_history_interval instead of leaving it to retry at the normal cadence. 2. The storage-unavailable path returned before that reset, so it was never throttled at all - every ~5-minute cycle re-entered and re-logged the same warning indefinitely. Fixed with a separate debug_history_storage_warned timestamp, deliberately independent of the capture throttle, so a later genuine capture attempt is never skipped just because the warning was recently logged. 3. debug_history_force_capture reset on trigger, before storage was even checked or a capture attempted - contradicting docs/customisation.md's "Predbat resets the switch back off itself once the snapshot has been taken." A failed forced capture now leaves the switch on, so it retries every cycle until it genuinely succeeds (or the switch is turned off), rather than the request being silently swallowed by an early reset. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…hot id Per @springfall2008's review on springfall2008#4438: Item 4 (race condition): html_debug_history_download's id=latest case called load_snapshot(storage, "latest") - which resolves "latest" internally but discards the resolved id - then called list_snapshots() a second, independent time just to rebuild the filename. A capture landing between the two calls could serve one snapshot's bytes under a different snapshot's filename. Added resolve_and_load_snapshot(), returning (resolved_id, text) from a single resolution, and used it in the route so the id used for the filename is always the same one the data actually came from. load_snapshot() now delegates to it, unchanged for existing callers. Item 6 (reflected XSS): "Snapshot {} not found".format(snapshot_id) reflected the raw ?id= query param unescaped into a text/html response. Escaped via html.escape(), matching the existing pattern used elsewhere in web.py (e.g. the plan-log highlighting). Item 21 (test coverage): the three debug-history web routes had zero tests. Added test_web_debug_history_routes.py covering all three (list/download/download-all) against a real StorageLocalFiles backend, including the specific race (item 4) and XSS (item 6) fixes above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…havior Per @springfall2008's review on springfall2008#4438 (items 8-9): docs described the dropdown snapshot picker and "nearest"/"how many snapshots ago" matching from an earlier design in this PR, replaced during development by a single "Download all (.tgz)" link and exact floored-slot matching (confirmed against the shipped JS - findNearestDebugSnapshot's tolerance is 1000ms, guarding only sub-second formatting noise, not a real nearest- within-a-window search; labels use absolute local time, not "N ago"). Updated docs/web-interface.md, docs/predbat-plan-card.md, and docs/customisation.md to describe what actually ships. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per @springfall2008's review on springfall2008#4438 (item 22): findNearestDebugSnapshot, loadDebugHistoryData, and the plan table's Debug column were untested. Follows test_plan_why_reason.py's established precedent for this kind of embedded-JS test - assert on the JS source text's structure directly (there's no JS engine in this suite), rather than executing it. Covers: the exact/tight-tolerance (not nearest-of-several) snapshot match, the fetch being wired to the Yesterday view switch rather than the frequent plan poll, and the Debug column linking by the matched snapshot's own id with a real time label - locking in the same shipped behaviour docs were just corrected to describe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…comment - docs/customisation.md: mention the 2-hour auto-disable next to the existing switch.predbat_debug_enable writeup (review comment on springfall2008#4438). - test_debug_history.py: same-minute-dedup test comment referenced a "Deselect all" chart-series regression that belongs to a different, unmerged branch - reworded to describe what this PR actually guards against (id/filename collision on the floored-to-minute snapshot id).
…reeze Export, enable it for FoxESS/FoxCloud (springfall2008#4207) Freeze Export previously forced charge_rate_now to 0 unconditionally and fell through into the general export-limit clip, so any PV genuinely exceeding load + the hardware export limit was modelled as clipped/lost - regardless of whether the real inverter actually routes it into the battery instead (confirmed on FoxESS's "Feed-in First" mode and independently on Sigenergy under active Force Export). inverter_can_charge_during_export already modelled this exact recapture for active Force Export windows; it was just never wired into the freeze path. Freeze Export now gets its own branch in prediction.py (and its C++ kernel mirror): the battery is never actively discharged, but once PV alone exceeds what load + the export limit can absorb, the genuine spillover charges the battery (capped at the overflow amount and the battery's own charge rate, not the full rate) rather than being clipped, when inverter_can_charge_during_export allows it. SoC still holds flat whenever the export limit alone can cover the surplus - only genuine overflow moves it. FoxESS and FoxCloud's support_discharge_freeze reverts to True (both are the same hardware via different connection methods) now that this is correctly modelled instead of disabled outright - supersedes the earlier fix that just turned Freeze Export off for them. Kernel parity revision bumped (6 -> 7) and all six platform binaries cross-rebuilt via zig. coverage/cases/random_results.json and predbat_debug_pre_saving1.yaml.expected.json regenerated - Freeze Export now recaptures more energy than before in scenarios where PV exceeds the export limit, so some plans shift to use it more. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ero entries - Sanitize WindowedFeatures and chunk conversions against NaN, Inf, and None - Use nanmean/nanstd and sanitize scales before np.maximum in feature/target normalization - Add gradient clipping and finite parameter checks in Adam optimizer - Preserve valid 0.0 values in database save/load roundtrips - Fix duplicate training execution and curriculum pass initialization tracking - Add robust unit tests for NaN resilience, zero preservation, and 90-day curriculum
… joining (springfall2008#4593) The auto-join loop read octopoints_per_kwh only to compute a display rate, never to decide whether the event was worth joining, so a zero-point event was join-attempted exactly like a real one. This matters now because the Octopus integration puts national Power Up (free electricity) events into the Power Down available_events set at 0 p/kWh (springfall2008#4548 point 5), so each one gets joined, rejected, and still fires a false "joined" notification. Adds a guard matching the existing Axle-conflict skip in the same loop: octopoints_kwh <= 0 skips with a log line and continue, before the join attempt. null keeps its existing meaning (rate not reported, default applies) - only an explicit non-positive rate is skipped. Mirrors the planning side's own saving_rate > 0 requirement in the joined_events loop that builds octopus_saving_slots - that check already existed, it just wasn't shared with the join side. New test_saving_session_zero_rate_skip covers zero, negative, positive and null rates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ed one Bottle Cap Dave's Octopus Energy integration renamed join_octoplus_saving_session_event to join_octoplus_power_down_session_event (ADR-0004). Both remain registered until the old one is retired in January 2027, so try the new name first and fall back to the old one on failure. Part of springfall2008#4548 (point 3). TODO(springfall2008#4599) tracks removing the fallback once the old service name is gone upstream.
…r Down/Up rename Octopus Energy integration v19.0.0 renamed the saving-session and free-electricity event entities to Power Down and Power Up (event.octopus_energy_ACCOUNT_ID_octoplus_power_down_events / ..._power_up_events). Both old and new entities remain registered until January 2027 (ADR-0004), and resolve_arg_re()'s regex matching against HA state keys is a one-off startup resolution, so broadening the 're:' alternation to match either name is enough - no runtime try-new-then-old logic is needed here, unlike the join service rename in point 3. Updates the octopus_saving_session/octopus_free_session regex in apps.yaml (default, coverage test fixture, and all 23 inverter templates that configure them), plus docs/energy-rates.md to mention the rename, the entity_registry_enabled_default=False gotcha, and the January 2027 cutoff. Part of springfall2008#4548 (point 2). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s user, not just low power mode The dawn light/dark split (calc_dawn, springfall2008#4557) only ever mattered when combine_charge_slots could merge a window across sunrise - with it off, find_charge_window already forces a break every charge_slot_split minutes (= plan_interval_minutes, the same granularity calc_dawn buckets at), so the split was already a no-op there. It was gated on set_charge_low_power instead, which meant combine_charge_slots users without low power charging never got it, even though the split also lets the plan optimizer charge just the dark portion of a combined window and skip the daylight portion where solar may cover the load - a benefit independent of low power charging. New calc_pv_light_dark() gates the split on combine_charge_slots directly. Also logs the calculated dawn time each time it runs, so it's visible whether the detected dawn looks sane. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Plan four seasonal anchor months and reconstruct the other eight from the solar curve the run already downloads, cutting a WhatIf run to about 2.5x faster. Curve chosen from a measured study over three recovered 12-month reference runs: affine in solar (per-day = a + b*PV) beats cyclic linear, Fourier and proportional-to-solar on per-month error, and is insensitive to which months are anchored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five 12-month references scored (3x Intelligent Go, Cosy, Agile, plus an Agile run at 6 samples/month as low-noise ground truth). Confirms solar_affine as the default and rejects the mean-rate regressor: against noisy references it looked best, halving Agile's annual error, but leave-one-anchor-out cross-validation shows it overfits badly (362% vs 38% held-out error on Agile) because 4 anchors against 3 parameters leaves one degree of freedom. Decomposing Agile's error shows interpolation contributes under 1% to annual savings; the large Agile figures are sampling noise already present in today's 12-month run (-8.5%/+17.8% against the 6-sample reference), which fast mode inherits rather than creates. Recorded as a pre-existing limitation bounding what the caveat text may claim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven tasks: config/CLI flag, the pure interpolation module, a shared included-status constant, engine wiring, web UI, a committed curve-scoring guard, and docs. Also commits the two reduced reference fixtures the curve guard scores against, built from the Agile (6 samples/month) and Cosy 12-month runs. Reduced to months, scenarios and monthly PV only - no plans, location, tariff URLs or keys. Verified solar_affine still beats linear on both (agile 18.39% vs 18.84%, cosy 9.56% vs 14.47%). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nsumers Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t mode Anchors are Mar/Jun/Sep/Dec, which measurably beat Jan/Apr/Jul/Oct on the savings figures (Cosy: -0.5%/+0.2% against -10.6% on the Predbat saving). Adds a volatility guard. Interpolation only holds while a month's economics follow the solar curve; on a tariff whose whole price level moves day to day the reconstructed savings are 20-30% out. The run measures the coefficient of variation of daily average import price over the anchor months and, above 0.10, silently becomes a full run rather than reporting a fast wrong number. Measured: Agile 0.21, Cosy 0.005. Verified end to end: Agile --fast produces a bit-identical full run (+0.0%/+0.0%), Cosy --fast reconstructs payback to 5.84 vs 5.81 years. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nths Checkbox in Advanced, parsed like the existing debug flag. Interpolated months are tagged in the month table and noted above the chart, following the rates-synthesised precedent, so an estimated month is never mistaken for a planned one. 'What this run used' distinguishes a run that used fast mode from one that asked for it and was given a full run instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… runs Asserts the volatility guard sorts the reference tariffs correctly (Agile 0.222 declined, Cosy 0.002 accepted) and that on the tariff it passes, four anchors reconstruct the savings to 0.48%/0.19%, with solar_affine beating linear on the Predbat saving (0.19% vs 4.38%) - which is why it is the default. Fixtures carry daily mean import rates so the guard threshold is regression-tested, not just the curve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
User docs gain a Fast mode section covering what it does, its measured accuracy, when to turn it off, and that volatile tariffs get a full run automatically; plus the --fast flag and the config key. Corrects two things the spec got wrong before implementation. Anchors are Mar/Jun/Sep/Dec, not Jan/Apr/Jul/Oct: per-month cost error does not separate the candidates but the savings figures do, sharply, because a saving is a difference between two reconstructed scenarios. And the 'under 1% on annual savings' claim was a mean across anchor sets that hid a worst case of 33%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…arging The myenergi component publishes a Zappi's plug status straight from the pilot state, and pilot states C1 and D1 normalise to "EV ready to charge" - a car that is plugged in and waiting for its slot. None of the apps.yaml templates carried that value in car_charging_planned_response, so Predbat read a plugged-in, ready car as not planned to charge and dropped it from the plan. Added it to every template that lists the myenergi-aware responses, commented examples included. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…h retry
Ten issues found by the whole-branch review, all in myenergi.py:
The cloud device-list cache never aged. meta_age_seconds was set to its
maximum in __init__ and to zero on refresh, and nothing in between ever
increased it, so GET /devices ran exactly once per process and a device
added, removed or renamed in the myenergi app stayed invisible until
Predbat restarted. It is now a wall-clock stamp compared against
CLOUD_DEVICE_LIST_MAX_AGE.
The direct transport checked for the X_MYENERGI-asn header before the HTTP
status, so a 503 from the account server surfaced as "check the hub serial
and API key" and sent a self-hosted user off to regenerate a perfectly good
key during a myenergi outage. The status checks now run first, matching
_resolve_asn, whose docstring already explained why.
Auto-configuration left car_charging_planned broken. Every apps.yaml
template ships a regex for that key targeting the third-party ha-myenergi
integration's entity names, which do not match the ones this component
publishes, so Predbat logged "failed to match - disabling this item" and
fell back to the car_charging_threshold heuristic. automatic_config now
wires the Zappi plug status sensors into it alongside car_charging_energy.
The /cgi-* command endpoints answer HTTP 200 whether or not they acted,
carrying the outcome in a {"status": N} body that send_boost and
cancel_boost discarded. A refused boost - an Eddi already at maximum tank
temperature has no pre-check at all - logged as a success and the switch
quietly flipped back on the next poll. A non-zero numeric status now
raises, and switch_event_handler returns the result rather than dropping
it, so a refusal reaches the run loop's "control failed" warning.
A 401 mid-poll raised MyEnergiAuthError and simply ended the cycle. The
proactive check_and_refresh_oauth_token only covers a token that has
reached its stated expiry, so a token revoked before then wedged the
component until restart. It now calls handle_oauth_401 and retries the poll
once, as fox.py, deye.py and solis.py do.
Also: a cloud device whose status call fails no longer costs the whole
poll; poll_seconds is capped at 30 minutes and the success timestamp is
stamped only by a cycle that actually polled, so the health check reflects
real API contact without a slow poll reading as a failure; the entity
lookup anchors on a whole prefix; number_event_handler is guarded on the
boost suffixes symmetrically with switch_event_handler; boost amounts round
rather than truncate and a target time is zero padded to HHMM; an
unsupported device kind is refused rather than sent an Eddi command; the
CLI connects before polling; and an aiohttp too old for DigestAuthMiddleware
is reported with an actionable message instead of a bare AttributeError.
Tests cover every one of these, including the three production behaviours
that previously had no coverage at all - the poll_seconds gate, the cloud
cache-staleness branch and the Eddi kind guard - plus a reason-label table
for the direct transport matching the cloud transport's. The two "unknown
entity" tests now load a known device first, so their lookup loop actually
runs. All fourteen fixes were mutation-checked: reverting any one of them
fails the suite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…vers The documented "known limitation" was empirically false. It claimed iboost_today is the midnight-to-now difference and so under-reports after a mid-day Eddi reset, and that car_charging_energy is unaffected. In fact fetch.py reads a series that has already been through minute_data_load(..., clean_increment=True) and clean_incrementing_reverse, which rebases the counter on a reset: a realistic Eddi day of 3.0 kWh yesterday and two sessions of 2.0 and 1.5 today, with a mid-day reset, runs through Predbat's own minute_data to exactly the right 3.5 kWh. The real limitation is narrower and was undocumented. A drop only counts as a reset once a sample reads zero or the fall exceeds 1 kWh, so a session ending below roughly 1 kWh without a zero sample is not rebased and its energy is lost - two 0.6 kWh sessions total 0.600 against a truth of 1.20. That loss is in the shared cumulative series, so it hits car_charging_energy exactly as hard as iboost_today, the opposite of what was documented. Corrected in docs/components.md, in the automatic_config docstring, and in the design record. Also corrected: auto-config now covers car_charging_planned, so the docs no longer claim more or less than it does; the "whichever matches your auth_method" gate does not exist, components.py's required_or is an unconditional either/or; the not-implemented controls are a reserved interface nothing can reach rather than something a user can attempt; the Eco/Eco+ pre-check is one specific check, not a general guarantee that no rejected call is ever made; the Eddi sensor's more consequential effect is the load subtraction gated on iboost_energy_subtract, which runs whether or not iboost is enabled; and auto-config latching until restart is now stated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses PR springfall2008#4658 review feedback. rates_export_override and rates_import_override no longer collide/replace on repeat when sent without an index; document that each distinct command is kept as a separate time window. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
is_running(): an install upgraded from before record_status() wrote a timezone-aware last_updated could still have the old naive format persisted in HA until the next record_status() call overwrites it - comparing that against a timezone-aware "now" raised TypeError. Now branches on whether the parsed value is naive. predbat_update_move(): was returning True unconditionally regardless of whether the mv chain actually succeeded. Now returns the real result. Kept clearing git_version.txt before the mv (not after) - that ordering is deliberate, per the PR's own review discussion, to close a hot-reload race where a new process could read a stale marker mid-move; moving the clear to "only after success" would reopen that race. The trade-off - losing the diagnostic marker on a failed mv - is low-stakes, since verify_staged_files() has already ruled out a bad download by that point. Adds test coverage for both: a naive/aware last_updated round-trip through is_running(), and a failed mv chain propagating through predbat_update_move()'s return value.
…ersions-copilot-followup Address Copilot review comments from springfall2008#4653
…ng-debug-history
Include token_hash in the myenergi activation gate. A refresh-only OAuth setup carries no access token, and initialize() already accepts that, so gating on key alone meant such a config never constructed the component at all. Reject unsupported device kinds in the cloud transport's boost and cancel paths rather than defaulting them to the Eddi body, matching the direct transport. Add 'ev ready to charge' to alphaess_cloud.yaml, which landed on main after this branch and carries the same car_charging_planned_response vocabulary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_make_web() stubs my_predbat.components with a SimpleNamespace, but my_predbat is shared across the whole run, so the stub outlived the test and broke every later test calling a real method on it - is_running() raised AttributeError on components.is_all_alive() in test_web_functions. Save and restore it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-device tolerance added for a single unreachable device also swallowed the case where every device failed: fetch_devices returned an empty list, run() read that as an empty site, kept the previous readings and stamped success. A site whose every device was erroring therefore reported healthy indefinitely. Raise instead when the transport knows about devices and reads none of them, so run() returns False, the error count rises and components.py's 60 minute staleness check eventually marks the component unhealthy. An account with genuinely no Zappi or Eddi still reports no devices rather than an error, and a partial failure still keeps the healthy devices visible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The docs, docstring and spec all said a session ending below roughly 1 kWh is under-counted only when no reading lands on zero, which implied a zero reading would rescue it. It does not: minute_data() smooths any fall under 1 kWh as a dip in the data (utils.py:565) before clean_incrementing_reverse() (utils.py:740) ever looks for a reset, so the zero is gone by then. State the behaviour as it is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rgi-component feat(myenergi): Zappi and Eddi monitoring, auto-config and boost controls
…ervice HA's call_service websocket command expects target (entity_id/device_id/ area_id addressing) as a sibling of service_data, not nested inside it. A service configured with the common target: entity_id: ... syntax (e.g. input_boolean.turn_on/turn_off for a generic Service API bridge) landed with target nested inside service_data, which HA rejects with invalid_format: extra keys not allowed @ data['target'] - silently blocking every charge/discharge service call while Predbat's plan and status kept reporting normally. Fixes springfall2008#4662
- Pop target from a copy of service_data, not the dict itself - it's the same object async_call_service_websocket_command() logs on failure, so popping in place was silently dropping target from that diagnostic. - Reword the docs note: only target is elevated to a top-level sibling field, other keys (entity_id, device_id, option, etc.) still travel inside service_data as before. - Extend the regression test to assert the original queued service_data is left unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…boolean-service-target-4662 fix(ha): send target as a sibling of service_data in websocket call_service
Adds export_limit_kw as a top-level WhatIf config field (default 10kW), combined via min() with whatever apply_hardware() already derives for export capability. Previously this only existed as battery.export_limit_kw, so a PV-only (no-battery) run had no way to model a grid export cap at all - only battery-equipped runs could. A legacy battery.export_limit_kw is still read as a fallback so existing YAML configs and stored runs keep working.
Adds myenergi_zappi_control. Inside a planned charging window Predbat puts the Zappi in Fast, and outside one in Stopped. Fast because the window was chosen for its rate rather than for sunshine - Eco or Eco+ would only charge from surplus and the car would not get what the plan assumed. Each Zappi follows its own car, matched in serial order, the same order automatic_config wires car_charging_energy and car_charging_planned in, so the two cannot disagree about which Zappi is which car. The mode already polled is compared against what was asked for and re-applied, since purely edge-triggered control diverges the moment anything else touches the charger. A component-level switch.predbat_myenergi_zappi_control hands the charger back without editing apps.yaml, on by default and persisted through Storage so a restart does not quietly resume control. That release, and Predbat's own read only mode, restore the mode the Zappi had before Predbat took over, falling back to Eco+ when nothing was saved - simply going quiet would strand a car Predbat had left Stopped. set_mode is implemented on both transports to support this, so it is no longer one of the reserved stubs and is now abstract on the transport interface. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exercises the same three supply-mode commands Predbat-led charge control issues, so the API can be tested against a real Zappi without enabling the feature. --release is included because without it --stop-charge would leave the charger sitting in Stopped with no way back from the command line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The move to a top-level field accidentally added exclusive_minimum=True, rejecting 0 - a regression from the original battery.export_limit_kw, which allowed 0 to model a zero-export grid connection. Flagged by Copilot review on PR springfall2008#4672.
Copilot review: the switch handler is gated behind myenergi_enable_controls, so the switch cannot be toggled in monitor-only mode. The symptom is real but wider than reported - switch_event() returns before the handler is ever reached, and publish_data gated only on the config key, so the switch appeared reading "on" for a feature enable_control() had already refused to start. Making it respond to a toggle would leave it live but still lying, so gate publishing on control_active instead: no switch appears unless control could actually act, and it reappears with its remembered state once controls are allowed. Also restore the saved state before the first publish rather than after. A restart with control switched off previously showed the switch on for one cycle and then flipped, which reads like Predbat taking control back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…f-grid-export-limit WhatIf: grid connection export limit, independent of battery
…rgi-zappi-control feat(myenergi): drive the Zappi from Predbat's car charging plan
…al-api-rate-override-dedup fix(manual-api): allow multiple overrides for lists (rates_import_override, rates_export_override)
Brings in ~250 upstream commits: myenergi integration, AlphaESS cloud
component, annual planner fast-mode (interpolated months), debug-history
snapshot buffer, MCP server hardening, and a large batch of test coverage
and doc updates - alongside this fork's ~130 commits (frozen car-SoC
guard, GivTCP discharge-target fixes, saving-session join/notify
handling, db_primary entity filtering, custom dashboard/mobile nav,
heat-pump annual modelling, kernel threads:0 fork-pool fix, etc).
FIT (Feed-in Tariff) dropped from the C++ kernel at the user's request:
upstream's ABI bump (4->5, PkScenario::soc_out nullable, pk_run_batch)
landed the same cycle the fork carried FIT fields in PkContext, so
keeping FIT would have meant hand-recompiling all 6 platform binaries
against a merged struct layout - not practical here, and the user no
longer needs the feature. Kernel now runs upstream's plain ABI 5/9
unmodified (source and prebuilt binaries); FIT's config/Python-engine
code in fetch.py/prediction.py/output.py/plan.py is untouched but inert
on this deployment since prediction_kernel_enable is on and the kernel
is the active engine.
Heat-pump annual modelling and upstream's new fast-mode interpolation
both touched the same per-month planning loop; resolved by gating fast
mode off whenever a heat pump is configured (annual.py) rather than
teaching the interpolator about a heat pump's demand curve.
Fixed three merge-induced regressions caught by the test suite
(285 tests, all green):
- prediction_kernel.py's ctypes PkContext still declared the 3 dropped
FIT fields, desyncing it from upstream's compiled struct and failing
kernel context creation on every scenario (kernel_parity, model_kernel,
prediction_batch, kernel_static_cache, optimise_windows_kernel,
debug_cases, perf, random all cascaded from this one).
- The Bottle Cap Dave join branch in octopus.py picked up upstream's
immediate call_notify on join-request; the fork's actual design
(confirmed by test_saving_session_notify_config) only notifies once
an event is confirmed in joined_events, gated on
octopus_saving_notified - restored that, kept the service-name
fallback (join_octoplus_power_down_session_event) upstream added.
- web_mcp.py's _execute_set_config auto-merged upstream's
self.set_state_external shorthand, but it also runs with self bound
to MCPServerWrapper (not a ComponentBase subclass) per
test_web_mcp.py - reverted to self.base.ha_interface.set_state_external,
which works for both.
Also reconciled a genuine fork/upstream disagreement: fork's
entity_has_no_value softens "unavailable"/"unknown" to an Info log with
no recorded error (get_arg_unavailable, motivated by entities like an
asleep car's SoC sensor); upstream's new get_arg_missing_index test used
"unavailable" as its example of a value that must still warn. Kept the
fork's softening and re-pointed upstream's test at a non-sentinel bad
value ("abc") instead, since both behaviours are legitimate and only
collided on the specific string chosen.
THIS_VERSION bumped to v712.25.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
prediction_kernel.cpp: clipped_today (and the pv_ac_before/pv_ac_no_loss
locals that fed it) only ever existed to feed the FIT income calculation
removed in the merge commit - dropping that left them set-but-never-read,
which -Werror correctly caught under clang (the CI cross-build), though
not under the native g++ used for the local compile-check. Removed all
four.
test_web_if.py: confirmed against springfall2008/batpred upstream
directly (not introduced by this merge) - web.py registers
/debug_history_list, /debug_history_download, /debug_history_download_all
and /images/{filename}, but upstream's own hand-written endpoint list
in this test never picked them up. Added them to the existing
sweep_exempt mechanism: the debug_history routes need a captured
snapshot to mean anything and are already covered by
test_web_debug_history_routes.py, and /images/{filename} is a path
parameter, not a literal fetchable URL.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Merges ~250 commits from
springfall2008/batpredupstream into this fork's main, alongside this fork's ~130 own commits. Full detail in the merge commit message; highlights below.New from upstream: myenergi (Zappi/Eddi) integration, AlphaESS cloud component, annual planner fast-mode (interpolates months instead of simulating all 12), debug-history snapshot buffer, MCP server hardening, kernel ABI bump (4→5: nullable
soc_out,pk_run_batch,PK_MAX_CARS4→8), plus a large batch of new test coverage and doc updates.FIT (Feed-in Tariff) dropped from the kernel at Dan's request — upstream's ABI bump landed the same cycle this fork had FIT fields living in
PkContext, and keeping FIT would mean hand-recompiling all 6 platform binaries against a merged struct layout, which isn't practical here. Kernel now runs upstream's plain ABI 5/9 unmodified (source + prebuilt binaries). FIT's config/Python-engine code (fetch.py/prediction.py/output.py/plan.py) is untouched but inert on this deployment since the kernel is the active engine — flagging in case you want it fully ripped out later rather than left dormant.Heat-pump annual modelling vs. fast-mode: both touched the same per-month planning loop. Resolved by forcing fast-mode off whenever a heat pump is configured (
annual.py) — heat-pump users keep full per-month simulation, everyone else gets upstream's new interpolation.Merge-induced regressions caught by the test suite and fixed (285 tests, all green after fixes):
prediction_kernel.py's ctypes struct still declared the 3 dropped FIT fields → kernel context creation failed on every scenario (cascaded into kernel_parity, model_kernel, prediction_batch, kernel_static_cache, optimise_windows_kernel, debug_cases, perf, random).octopus.pypicked up upstream's immediatecall_notifyon join-request, double-notifying alongside the fork's actualjoined_events-polling design (octopus_saving_notified). Restored fork's notify-on-confirmation-only behaviour; kept upstream's service-name fallback (join_octoplus_power_down_session_event).web_mcp.py's_execute_set_configpicked up upstream'sself.set_state_externalshorthand, which breaks whenselfis aMCPServerWrapper(not aComponentBasesubclass) pertest_web_mcp.py. Reverted toself.base.ha_interface.set_state_external.entity_has_no_valuesoftens "unavailable"/"unknown" to Info-not-error (get_arg_unavailable, e.g. for an asleep car's SoC sensor); upstream's newget_arg_missing_indextest used "unavailable" as its example of a value that must warn. Kept the fork's softening, re-pointed upstream's test at "abc" instead.THIS_VERSIONbumped to v712.25.Test plan
--quick, excludingweb_if) — all passingprediction_kernel.cppcompiles cleanly with-Wall -Werror(scratch build, not committed)🤖 Generated with Claude Code