You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Smart discharge via hardware export-limit actuator (SmartDischargeExportLimitSensor, discharge_export_limit_w attribute, new Max Grid Export Limit Entity mapping in entity mode): when a foxess_modbus "Max Grid Export Limit" number entity is mapped, smart discharge modulates feed-in by writing the inverter's hardware export cap each tick instead of mutating the cloud schedule's fdPwr. The cloud schedule is pinned at inverter max; the actuator is written with the paced target, clamped to [peak_consumption × 1.5, Grid Export Limit] — the C-001 safety floor is the lower bound. Sub-threshold deltas are suppressed to avoid chatty modbus writes. Every session exit (timer expire, circuit-breaker abort, SoC threshold, feed-in limit, manual cancel) reverts the entity to the configured maximum. Opt-in: unchanged when no actuator entity is mapped. Simulator gained max_grid_export_limit_w with curtailment physics for E2E coverage.
Deferred-phase slack on the control card: while a smart charge or discharge session is in the deferred phase (window open, pacing algorithm waiting to act), the card shows a "slack" row giving the algorithm's live countdown to forced action — the same deferred_start − now the listener recomputes each tick, so solar surplus grows slack and load spikes shrink it. New charge_time_slack_s / discharge_time_slack_s sensor attributes (integer seconds, null outside the deferred phase). Translated into all 10 card languages.
Charge re-deferral when ahead of schedule: during the active-charge phase, if solar pushes SoC ahead of the pacing trajectory, the listener re-enters the deferred phase (stops forced charging) and re-evaluates each tick; it only resumes forced charging when the deadline requires it. Previously, once charging_started, the listener only adjusted power (min 100 W) and could not revert to self-use — causing the target to be reached 30+ minutes early, wasting cheap-rate self-use time.
Unreachable charge target surfaced as an HA Repair issue: when the pacing algorithm detects the target cannot be reached in the remaining window, an HA Repair is raised (dismissed automatically once reachable again or when the session ends). Paired with an outlier-robust feasibility check that blends the taper-integrated estimate with a median-ratio linear estimate — prevents false alarms when a few isolated low taper observations skew the integrated estimate, while still catching genuine unreachability.
Distinct scheduled / deferred / charging phases on the charge side (to match the discharge side's scheduled / deferred / suspended / discharging): the UI no longer conflates "window not yet open" with "window open, pacing deferred". charge_phase now reports scheduled before the window opens and deferred during the window when forced charging has been pushed later; new charge_deferred translation key wired into all 10 card languages.
Structured event emission + replay harness for pacing-algorithm regression testing: algorithm decisions, tick snapshots, service calls, session transitions, schedule writes, and taper updates are emitted through the logging pipeline as JSON-serialisable records (smart_battery/events.py). smart_battery/replay.py re-invokes each algo_decision with its recorded inputs and flags divergences; tests/test_replay.py::test_committed_trace_replays_clean parametrises over any JSONL in tests/replay_traces/, making traces executable regression protection against algorithm drift. The existing info/debug log sensors capture events for out-of-process collection via scripts/collect_events.py.
Nightly soak test suite (tests/soak/, 19 scenarios: basic charge/discharge, solar interaction, spiky load, BMS high-SoC taper, cold battery, large battery, tight windows, extreme taper, very-cold current limiting, combined charge-then-discharge cycle, solar-exceeds-load/target, near-min-SoC, heavy-load-during-deferral): runs full sessions through containerised HA + simulator, verifying SoC-overshoot, no-import, and target-reach invariants. Simulator auto-tick loop (5 s steps) advances the model in real time so sessions progress without explicit fast-forward. Results land in a SQLite soak_results.db with state transitions, SoC direction changes (2% deadband), and power-step changes as discrete events — enabling cross-run comparison between tags. Systemd timer runs the latest tagged release nightly.
Grid Export Limit configuration option (default 5 kW, 0–20 kW): net export cap set on the inverter (DNO / firmware limit). When non-zero, smart discharge deferral treats it as the effective maximum export rate and the active-discharge phase requests inverter max power.
Fixed
Deferred-start feed-in headroom over-defers on export-limited sites (beta.14, surfaced by live session monitoring): calculate_discharge_deferred_start previously applied the doubled feed-in headroom (up to 40%) unconditionally whenever feedin_energy_limit_kwh was set. The doubled margin is justified when household load volatility can erode effective export, but when Grid Export Limit is configured well below inverter max power, load must exceed max_power − grid_export_limit before net export degrades at all — so on many sites the doubled buffer was protecting against a scenario the hardware made physically impossible, eating ~3 min of self-use time per session. Now conditional on whether max(net_consumption_kw, consumption_peak_kw) exceeds the clamp slack: if the clamp actively shields export rate, single headroom (10%) is used; otherwise doubled headroom still applies. Six-test regression suite TestFeedinHeadroomAccountsForExportClamp covers live-session parameters plus neighbourhood (unlimited export, boundary limit == max_power, peak above/below slack, net-consumption variant, C-001 floor preservation).
Discharge pacing wasted energy when export-limited: when a Grid Export Limit was configured, power pacing reduced discharge power below inverter maximum even though firmware already caps grid export, under-utilising the battery's contribution to house load. Now uses maximum inverter power during active discharge and relies on deferral timing for energy management.
Taper profile blind to BMS curtailment during paced charging (beta.1): _record_taper_observation used the paced power request as the denominator instead of inverter max. When pacing reduced the request below the BMS limit (e.g. 4552 W paced vs 6380 W accepted at 81% SoC), the ratio exceeded 1.0, was clamped to 1.0, and the profile silently recorded "no taper" at high SoC. Subsequent sessions had no taper data for the 80–100% band, producing inaccurate time estimates and deferred start calculations.
Spurious "unreachable charge target" repair issue caused by outlier taper observations (beta.10): taper-integrated estimate summed isolated outliers (bins 81: 0.05, 83: 0.41, 85: 0.16, 90: 0.21 surrounded by 0.87–1.0 neighbours) to produce 1.04 h of taper-weighted charge hours, pushed over 1.09 h by the 10% headroom buffer, and fired a false Repair. Fixed by blending taper-integrated with median-ratio linear estimate and taking the minimum — genuine unreachability still flagged because the median is also low in that case.
Sensor countdown / phase labels mis-matched listener decisions (beta.10 / beta.12): the sensor-side deferred start estimate used a simplified formula without headroom, taper profile, net consumption, or BMS temperature, so the card showed wrong countdown times and wrong phase labels ("Charge Scheduled" when charging had started; "0 m" or "39 m" instead of ~24 m). is_effectively_charging(), estimate_charge_remaining(), and estimate_discharge_remaining() now call the same algorithm with the same parameters the listeners use (C-038 sensor-listener parameter parity).
Deferred-phase charge card SoC hidden at high precision (beta.11): during the deferred charge phase the progress bar is suppressed, leaving only the target row. That row previously rendered Math.round(current) → target, hiding the two-decimal interpolated value. Now uses two-decimal rendering matching the progress bar once the interpolated value drifts from the last confirmed integer.
Info log and init debug log entity IDs (beta.7 regression, beta.9 fix): removing _attr_name left these sensors relying on translation-driven naming, but the translation keys (info_log, init_debug_log) were only present in strings.json — not translations/en.json — so HA fell back to device-name and registered them as sensor.foxess_2 / sensor.foxess_3. Added the missing translation entries plus a regression test that asserts every log sensor with _attr_has_entity_name = True has either an explicit _attr_name or a resolvable translation key.
Overview card crash on corrupted box entries: card threw an uncaught TypeError when _boxes contained entries with unexpected shape (e.g. {flow_from: [...]} from energy-dashboard config patterns or corrupted state). _renderBox() now skips null/undefined/typeless entries, and _render() catches exceptions with a graceful error fallback UI.
Poll timer not reset on deferred session creation: creating a deferred charge or discharge session didn't trigger a coordinator refresh, leaving the next poll up to 300 s away. Now calls async_request_refresh() immediately so the UI updates within seconds.
Session context missing from debug log records (beta.8): SessionContextFilter was installed on the custom_components.foxess_control logger, but Python's logging module does not run parent-logger filters on records emitted from child loggers (e.g. smart_battery.listeners). Beta.7's new structured events landed in the debug-log sensors with session: None. Fixed by attaching the filter at the handler level so every record seen by the debug-log handlers is enriched with the current session context regardless of emitting logger.
Division-by-zero guards in calculate_deferred_start, is_charge_target_reachable, and calculate_discharge_deferred_start for edge cases where effective charge/export power or the headroom denominator is zero.
Production assert statements replaced with RuntimeError (10 occurrences across listeners.py, config_flow.py, services.py), preventing silent AssertionError in optimised builds.
Narrowed exception handling in config_flow.py: three except Exception catches narrowed to (OSError, requests.RequestException) or (FoxESSApiError, requests.RequestException, OSError) so unexpected errors are no longer masked.
Cross-field config validation: discharge now rejects start when current SoC is at or below min SoC, with a descriptive ServiceValidationError.
REST poll cadence preserved during WS injections: WebSocket-driven data updates no longer reset the coordinator's REST poll timer, so scheduled polls fire on their intended cadence regardless of WS activity.
Improved
Flaky Test Detection workflow triggers on tag push: the workflow previously ran on workflow_run of whatever pushed to main, so nightly detection repeatedly exercised stale commits from before the session. Switching to push: tags: [v*] makes the workflow run against each released prerelease.
E2E page fixture tolerates navigation churn and slow containers: the page fixture's wait for ha-panel-lovelace was failing on GH-hosted runners because either (a) HA's frontend navigations destroyed the JS execution context mid-poll (beta.12: deadline-bounded retry on Execution context was destroyed / navigating only), or (b) the panel legitimately took longer than the 30 s monolithic cap to boot (beta.13: split _wait_for_lovelace_panel into three staged waits — home-assistant → home-assistant-main → ha-panel-lovelace — each retrying on context destruction with min(remaining_budget, 30 s) per stage, total budget 75 s).
Soak test container name collision: concurrent soak runs (nightly timer firing while a manual run is still active) now use PID-prefixed container names for isolation.
Documentation
Priority hierarchy (P-001…P-007) across the knowledge tree (beta.14): docs/knowledge/01-vision.md now has an ordered ## Priorities section (no grid import > min SoC > energy target > feed-in > operational transparency > brand portability > engineering process integrity). Every C-NNN cites the P-NNN it enforces; every D-NNN cites the P-NNN it serves, any lower-priority goal it trades against, and a safety/pacing/other classification. 05-coverage.md gained a priority-enforcement matrix and trade-off audit that made one pre-existing priority inversion visible (D-005 corrected). CLAUDE.md surfaces the priority list as a top-level section.
D-047 Hardware export-limit actuator documented with the two-channel control scheme (cloud schedule at max; actuator modulated each tick with threshold-gated writes, clamped to C-001 floor). 03-architecture.md InverterAdapter section documents set_export_limit_w / get_export_limit_w; structural repair of a prior edit that had inserted Soak Test Infrastructure mid-External-Dependencies table; BMS-temperature endpoint reference corrected to /dew/v0/device/detail.
README: new Grid Export Limit option row, Max Grid Export Limit Entity mapping, sensor.foxess_discharge_export_limit, charge_time_slack_s / discharge_time_slack_s attributes, expanded charge_phase / discharge_phase enum, charge re-deferral paragraph, and unreachable-target Repair blurb.