Skip to content

v1.0.18

Choose a tag to compare

@github-actions github-actions released this 02 Jun 04:31
b7c7e95

Fixed

  • Smart discharge imported from the grid at end-of-session when battery capacity is not configured (P-001 / C-002 / C-025). battery_capacity_kwh defaults to 0 and has no required config-flow field, so any user who hasn't configured it runs smart discharge with pacing disabled — which also skipped the suspend/end-of-discharge guard. The session's only stop mechanism was a 2-consecutive-tick confirmation that removed the ForceDischarge override roughly one tick (~60 s) after SoC first reached min_soc. On the cloud path the active ForceDischarge group carries fdSoc = min_soc, and FoxESS ForceDischarge stops following house load at fdSoc — so during that ~1-tick window the inverter held the floor by importing house load from the grid (the live 2026-06-01 ~1.4 kW / ~2 min import). Fix: _check_soc_threshold now removes the override and reverts to self-use on the first tick SoC reaches min_soc — capacity-independent (needs only soc <= min_soc), closing the import window for both cloud and entity backends, while the session teardown still keeps the 2-tick anti-flap confirmation (reverting to self-use early is harmless on a transient dip — self-use serves load from the battery and the session resumes if SoC recovers; the latch re-arms). The capacity-known path is unchanged: the C-017 guard already removes the override while SoC is strictly above min_soc, so the at-floor removal is idempotent there. Regression test tests/test_discharge_fdsoc_floor_import_no_capacity.py drives the real cloud discharge tick with capacity unset and asserts no tick is left in ForceDischarge at/below min_soc (RED reproduced the import window); a companion proving test tests/test_discharge_fdsoc_floor_import.py documents that the capacity-known path was already safe. The 2026-06-01 import observed on a cloud install was this bug compounded by the (separately fixed in 1.0.17) full-power pacing bug that drove SoC into the floor early.
  • WebSocket lagged the smart-charge deferred→active transition by ~4 minutes (C-020). Live observation 2026-06-02 (on v1.0.17): smart charge flipped scheduled → charging at 01:00:10Z, but sensor.foxess_data_freshness stayed api until 01:04:04Z (~3m54s) before switching to ws. This is the same ~4-minute symptom 1.0.17-beta.2 targeted, but a deeper cause: the beta.2 fix made the WS-startup hook (on_session_started) event-driven and correctly wired — but the listener's transition itself was not. _adjust_charge_power only re-evaluated the deferred→active transition on its periodic async_track_time_interval (SMART_CHARGE_ADJUST_SECONDS = 300 s), with no point-in-time wake at the committed deferred-start deadline. So the status sensor flipped to charging immediately (recomputed every ~5 s coordinator refresh) while the listener didn't set charging_started=True / fire the hook until its next 300 s tick — up to one full interval late. Fix: _schedule_deferred_wake schedules a one-shot async_track_point_in_time at the committed deferred-start time, re-running _adjust_charge_power through the session-id guard + circuit breaker. Idempotent (no-op for the same deadline, reschedules on D-043 re-defer, cancelled at the transition). Brand-agnostic — the wake lives in smart_battery/ and drives the existing on_session_started Protocol hook, so C-039 dependency inversion is preserved (semgrep-enforced). New regression class tests/test_ws_startup_charge_transition.py::TestTransitionFiresPromptlyAtDeferredDeadline captures the point-in-time registrations and asserts a wake is scheduled at the deadline (reproduces the live ~4-min lag as RED). The discharge deferred-start path has the analogous structural gap but additional WS-startup triggers (per-tick WS-aware wrapper, WS-message arrival, auto-mode targeting) make it lower-severity — flagged as a follow-up.
  • Entity mode: smart discharge left the inverter's persistent Min SoC floor at the session target, causing grid import after the session ended (P-001 / P-002 / C-025). In entity mode (foxess_modbus backend), FoxESSEntityAdapter.apply_mode wrote the session discharge target (fd_soc, e.g. 50%) into the inverter's persistent on-grid Min SoC register during force discharge, and never restored it on the self-use teardown. When the session ended and reverted to self-use, the inverter refused to discharge the battery below that raised floor and served house load from the grid instead. This conflated two distinct concepts: the session's force-discharge stop SoC vs the inverter's persistent self-use reserve. Fix: the entity adapter now restores the configured reserve (min_soc_on_grid) to the Min SoC entity on any non-discharge mode (self-use teardown / force charge / feed-in); force discharge still writes fd_soc as the stop SoC. The configured reserve is plumbed into FoxESSEntityAdapter at both construction sites. The cloud adapter was already correct (it keeps minSocOnGrid at the configured reserve while per-group fdSoc carries the target), so this affected entity-mode users only. Regression class tests/test_entity_mode_write_units.py::TestPersistentMinSocFloorRestored drives the real adapter apply_mode → remove_override and asserts the teardown Min SoC write equals the reserve, not the target; an inverse case guards that active discharge still writes the target stop SoC (C-002 preserved). C-008 (fdSoc >= 11, minSocOnGrid <= fdSoc) unaffected.

Tests

  • De-flaked test_pv_values_consistent_with_solar_total (C-031). The E2E test gated its wait on the backend sensor (sensor.foxess_solar_power >= 2.5) but then asserted on the overview card's solar node text in the shadow DOM — so it could read the placeholder before the card re-rendered with fresh coordinator data, failing with ValueError: Cannot parse power from '—'. The wait now polls the card DOM until the rendered solar value and PV detail match the same power pattern the parser uses (gate on the observable being asserted, not a backend proxy). Test-only; assertion meaning unchanged.