Skip to content

v1.0.17

Choose a tag to compare

@github-actions github-actions released this 01 Jun 23:36
a76709a

Consolidates the 1.0.17-beta.1 … beta.9 pre-releases plus the smart-discharge
pacing fix below.

Fixed

  • Smart discharge ran at full power instead of pacing, draining to min SoC early and then importing from the grid (P-001 / C-001 / C-037 / D-047). Live observation 2026-06-01 (Sydney evening peak, session 44b31626): a 2-hour discharge session held forced discharge at ~6.5–7.5 kW for the entire window, drained 76% → 50% (min SoC) ~15 minutes before the window end, and the instant forced discharge dropped out at min SoC the house began importing 1.4 kW from the grid — a violation of P-001, the top-priority no-import invariant. Root cause: the two-channel hardware-export-actuator scheme (D-047) pins inverter power (fdPwr) at the maximum and modulates feed-in via the export-limit actuator — but three power gates selected that scheme based on whether a grid-export-limit value was configured (grid_export_limit > 0, default 5000 W) rather than whether an export-limit actuator entity was actually present. On installs with an export limit configured but no actuator entity, fdPwr was pinned at max, the actuator write never fired (no entity), and the software pacing in calculate_discharge_power was bypassed entirely — so discharge ran flat-out to min SoC. Fix: all three gates (_start_deferred_discharge and _apply_discharge_power in smart_battery/listeners.py, and _do_smart_discharge in smart_battery/services.py) now gate on actuator-entity presence (_has_export_limit_entity) instead of the limit value. Without an actuator, discharge falls back to software pacing exactly as it does when no export limit is set. Regression suite tests/test_export_limit.py::TestExportLimitConfiguredButNoActuatorPaces drives the real discharge listener tick and asserts paced power (not max) is written when a limit is configured without an actuator, that the actuator scheme still pins-and-modulates when an entity is present, and that a tight window still legitimately paces toward max.
  • WebSocket reconnected after natural session-end and stayed alive indefinitely (C-020). Confirmed via WS-DIAG trace 2026-05-31: a clean smart-discharge session ended, _stop_realtime_ws ran and began its 30 s linger phase — but during that linger the listen loop hit a stale-detect / server-CLOSED frame and called _try_reconnect, whose _stop_event guard passed (that event is only set later by async_disconnect, after the linger). A fresh connection was re-established before disconnect fired. Result: WS stayed alive for hours after session end (16+ hours of continuous frames observed during pure idle), compounded by the linger's plausibility filter dropping post-session frames so the linger always ran its full 30 s. Fix: a separate _no_reconnect flag on FoxESSRealtimeWS distinct from _stop_event, set synchronously by a new request_stop() that _stop_realtime_ws calls before the linger begins; _try_reconnect checks it at all three yield points. Linger semantics unchanged; only the reconnect path is suppressed during the session-end window. Guarded by an E2E test (polls data_source for 60 s after natural end, asserts every reading is "api") and a soak-test invariant (check_ws_disconnected_after_session_end).
  • sensor.foxess_data_freshness flapped between ws and api every ~5 minutes during normal operation (C-020). The badge oscillated ws → api → ws on the 300 s REST-poll cadence while WebSocket frames arrived normally, because _async_update_data unconditionally set _data_source = "api" after every poll even when the WebSocket was live. Fix: the coordinator records the monotonic timestamp of every WS injection and, when the WS object reports is_connected and the most recent inject was within _WS_FRESH_INJECT_WINDOW_S = 30 s, preserves _data_source = "ws" through the REST poll. Stale or absent WS still degrades cleanly to "api". Covered by tests/test_coordinator.py::TestDataSourceTracking.
  • WebSocket lagged the smart-charge deferred→active transition by up to 5 minutes (C-020). A session flipped deferred → charging but the WebSocket did not connect until up to one full SMART_CHARGE_ADJUST_SECONDS (300 s) interval later, leaving the dashboard on stale REST data while charging at full power. Fix: a new on_session_started hook on the InverterAdapter Protocol, called synchronously when charging_started / discharging_started flips True; the cloud adapter schedules _maybe_start_realtime_ws immediately, the entity-mode adapter is a no-op. Dependency-inverted per C-039 (semgrep-enforced). Covered by tests/test_ws_startup_charge_transition.py.
  • charge_target_unreachable Repair issue: stale-persistence, bare-key rendering, and missing localisation (C-020 / C-022 / C-026). Three related defects, fixed across the beta series: (1) the issue could persist after the algorithm recovered (solar-driven D-043 re-defer paths exited before the post-adjust clear) — a closure-local helper now re-checks reachability before each early return and clears the issue when the target becomes reachable; (2) the issue rendered as the bare translation key because HA loads runtime UI strings from translations/<lang>.json, not strings.json, and the issues/exceptions blocks were missing there — both blocks are now mirrored into translations/en.json (fixing three other latent keys too) and the listener forwards translation_placeholders (current SoC, target, remaining hours, max power) so the message is actionable; (3) the issues and exceptions blocks are now localised to all nine non-English locales (de, es, fr, it, ja, nl, pl, pt, zh-Hans), with placeholders preserved character-for-character. Regression suites: tests/test_smart_charge_unreachable_clear.py, tests/test_runtime_translations_issues.py.
  • E2E Playwright flakiness hardening (C-031). page.evaluate() / wait_for_function calls in E2E test bodies bypassed the context-destruction retry helpers, surfacing as Execution context was destroyed failures (a beta.4 release-gate blocker). Audited and wrapped all 29 unprotected call sites; extended _safe_evaluate with an arg= parameter and added _safe_wait_for_function; two semgrep rules plus an AST-based unit test (tests/test_e2e_helper_usage_policy.py) now fail pre-commit on any direct primitive call outside the helper allowlist. Also bounded per-retry settle waits to 3000 ms domcontentloaded, and fixed the _tight_window test helper so it can no longer yield a same-day window straddling midnight (C-009) — it sleeps to 00:00:05 of the next UTC day when no fittable window exists.

Added

  • sensor.foxess_charge_slack — minutes of margin to reach the charge target during an active smart-charge session (C-020, C-038). A continuous reachability metric for automations to threshold against (e.g. "when slack < 10 minutes, cut house load"), complementing the binary charge_target_reachable flag: positive = comfortable margin, zero = exactly on the deadline at full power, negative = unreachable. Available only during the charging phase; unavailable otherwise. The sensor and the listener's reachability check both compute buffered_hours from a shared _buffered_charge_hours helper with the same parameter list, enforcing C-038 sensor-listener parity by construction. Localised across all ten translations. Covered by tests/test_charge_slack_sensor.py (31 tests).

Diagnostic

  • WS-DIAG:-tagged diagnostic logging added during the beta series to trace the WebSocket-stays-alive-during-idle leak (since fixed above) is still present in this release, pending removal in a follow-up once the fix is confirmed in production. It emits WARNING-level stack traces from _maybe_start_realtime_ws / _stop_realtime_ws and a connection-state tuple from the coordinator on each REST poll; visible via the debug-log sensors.