v1.0.17
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,fdPwrwas pinned at max, the actuator write never fired (no entity), and the software pacing incalculate_discharge_powerwas bypassed entirely — so discharge ran flat-out to min SoC. Fix: all three gates (_start_deferred_dischargeand_apply_discharge_powerinsmart_battery/listeners.py, and_do_smart_dischargeinsmart_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 suitetests/test_export_limit.py::TestExportLimitConfiguredButNoActuatorPacesdrives 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_wsran 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_eventguard passed (that event is only set later byasync_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_reconnectflag onFoxESSRealtimeWSdistinct from_stop_event, set synchronously by a newrequest_stop()that_stop_realtime_wscalls before the linger begins;_try_reconnectchecks 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 (pollsdata_sourcefor 60 s after natural end, asserts every reading is"api") and a soak-test invariant (check_ws_disconnected_after_session_end). sensor.foxess_data_freshnessflapped betweenwsandapievery ~5 minutes during normal operation (C-020). The badge oscillatedws → api → wson the 300 s REST-poll cadence while WebSocket frames arrived normally, because_async_update_dataunconditionally 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 reportsis_connectedand 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 bytests/test_coordinator.py::TestDataSourceTracking.- WebSocket lagged the smart-charge deferred→active transition by up to 5 minutes (C-020). A session flipped
deferred → chargingbut the WebSocket did not connect until up to one fullSMART_CHARGE_ADJUST_SECONDS(300 s) interval later, leaving the dashboard on stale REST data while charging at full power. Fix: a newon_session_startedhook on theInverterAdapterProtocol, called synchronously whencharging_started/discharging_startedflips True; the cloud adapter schedules_maybe_start_realtime_wsimmediately, the entity-mode adapter is a no-op. Dependency-inverted per C-039 (semgrep-enforced). Covered bytests/test_ws_startup_charge_transition.py. charge_target_unreachableRepair 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 fromtranslations/<lang>.json, notstrings.json, and theissues/exceptionsblocks were missing there — both blocks are now mirrored intotranslations/en.json(fixing three other latent keys too) and the listener forwardstranslation_placeholders(current SoC, target, remaining hours, max power) so the message is actionable; (3) theissuesandexceptionsblocks 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_functioncalls in E2E test bodies bypassed the context-destruction retry helpers, surfacing asExecution context was destroyedfailures (a beta.4 release-gate blocker). Audited and wrapped all 29 unprotected call sites; extended_safe_evaluatewith anarg=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 msdomcontentloaded, and fixed the_tight_windowtest 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 binarycharge_target_reachableflag: positive = comfortable margin, zero = exactly on the deadline at full power, negative = unreachable. Available only during the charging phase;unavailableotherwise. The sensor and the listener's reachability check both computebuffered_hoursfrom a shared_buffered_charge_hourshelper with the same parameter list, enforcing C-038 sensor-listener parity by construction. Localised across all ten translations. Covered bytests/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_wsand a connection-state tuple from the coordinator on each REST poll; visible via the debug-log sensors.