Skip to content

v1.0.14

Choose a tag to compare

@github-actions github-actions released this 30 Apr 23:58
23edd7d

Fixed

  • Control and taper cards showed "No active operations" on non-English Home Assistant installs (C-020 violation): during an active smart charge session, a user running HA in German saw "Keine aktiven Vorgänge" on the control card despite charge_active=true and charge_phase=scheduled on the backing sensor. The cards hardcoded sensor.foxess_smart_operations as the operations_entity default, but HA derives entity_ids from the translated friendly name at entity-creation time — in DE the real entity is sensor.foxess_intelligente_steuerung, in FR sensor.foxess_operations_intelligentes, etc. The integration already exposes foxess_control/entity_map for role-based discovery, and the forecast/history cards already consult it via a _resolve(key) helper; the control and taper cards ignored it. Users had no way to determine system state from the UI alone, requiring entity-registry inspection — a direct C-020 violation. Fixed by routing both cards through a _resolve(key) helper matching the forecast/history pattern: explicit operations_entity: config > _entityMap["smart_operations"] from the WS command > hardcoded English default as last-resort fallback. The taper card previously didn't fetch the entity map at all; it now does. Backwards-compatible for users with explicit dashboard YAML overrides. Seven-test regression suite (tests/test_card_entity_resolution.py) drives the card JS in a Playwright-chromium stub, reproducing the exact DE-locale symptom — four cases fail against pre-fix card code, three passed throughout (backwards-compat + graceful-degradation + source-level guards that catch any future regression into direct this._config.operations_entity reads).
  • Charge-phase sensor oscillated rapidly between charging and deferred during an active session: observed 2026-04-27 on a live charge session (11:00–13:59 window) where sensor.foxess_smart_operations flipped phase many times per minute (including a 5-second flip at 02:39:01 → 02:39:36) while the inverter's actual work mode only transitioned twice in the same 3 hours. The control-card title ("Smart Charge" vs "Charge Deferred") faithfully tracked the sensor's thrashing state, so the user saw the card lag reality by a couple of minutes after the algorithm had actually re-deferred — the sensor kept flipping back to charging as inputs jittered. Root cause: is_effectively_charging() in smart_battery/sensor_base.py independently recomputed calculate_deferred_start() from live coordinator data on every ~5s WS refresh; net_consumption_kw jitter of ±1 kW (appliances cycling, solar flicker) swung the computed deferred_start by 10–30 minutes tick-to-tick, crossing the now >= deferred threshold in both directions. SoC jitter of 0.1% (BMS reporting granularity / interpolation noise) had the same effect. Fix: the listener now commits its calculate_deferred_start() result to the session state (deferred_start_committed) on every tick; the sensor reads that committed value instead of recomputing. Same formula on both sides (preserving C-038 sensor-listener parameter parity), but the sensor is now a stable read-only view of the listener's most recent decision — no sub-minute volatility. Four-test regression suite (TestIsEffectivelyChargingStability) drives is_effectively_charging() with realistic input-noise sequences (±0.1% SoC, ±0.4 kW consumption) and asserts no phase flip under noise; neighbourhood tests confirm real qualitative changes still flip phase promptly.
  • WebSocket-driven power sensors flashed to ~20% of true value on single frames: production incident 2026-04-27 during a smart discharge session — the display sensors for discharge power, grid export and house load showed 49 single-sample dips within a 2-hour window (11 below 2 kW, down to 0.82 kW while real output was ~5.4 kW). Root cause was a mix of partial/stale ~5 s WS frames and energy-counter quantisation glitches. The control loop was unaffected because it reads unfiltered gridConsumptionPower, which stayed at 0 throughout — C-001 (no grid import) held. Fix applies a 3-sample rolling median in the per-sensor display path (FoxESSPolledSensor.native_value) to the seven WS-fed power channels (batChargePower, batDischargePower, loadsPower, pvPower, gridConsumptionPower, feedinPower, meterPower). Cumulative energy counters, SoC, voltage, current, temperature and frequency are NOT filtered — smoothing would distort their semantics. Display-only: coordinator.data retains raw values; listeners and safety checks continue to read unfiltered data directly via _get_coordinator_value (C-038 parity preserved — the filter sits below the listener-formula-parity boundary). Window semantics: fewer than 3 samples → display = most recent sample (fresh data never delayed waiting for history); 3 samples → display = median; any raw None surfaces as unavailability rather than being papered over.
  • Misleading wording in charge-deferral reason: the deferred-charge reason text previously read "solar surplus or cheaper-later pricing; waiting to start forced charge". The integration has never had tariff awareness (tariff optimisation is an explicit non-goal per the vision), so "cheaper-later pricing" was factually incorrect. Simplified to describe only what the integration actually reasons about: solar surplus and deferred-start math.

Test infrastructure

  • Screenshot and card-injection retry on transient DOM / execution-context races (flaky-detection hardening): _screenshot_card and the taper-card page.evaluate injection in tests/e2e/test_ui.py now wrap their Playwright calls in retry helpers (_safe_screenshot / _safe_evaluate) that catch "Element is not attached to the DOM", "Execution context was destroyed", and "Target closed", wait for networkidle, and retry with a freshly resolved locator/context. Mirrors the existing _find_card retry pattern so all gallery-screenshot and card-injection tests inherit the recovery in one place; unrelated errors (genuine Timeout, etc.) propagate unchanged. Each helper has deterministic stub-based unit coverage for the retry, re-resolve, and error-passthrough contracts.
  • pytest-playwright event-loop leak under -n auto (C-031 flake eliminated): pytest-playwright's session-scoped playwright fixture calls sync_playwright().start(), which keeps a greenlet-backed asyncio event loop pinned as "running" on the main thread until end-of-session. Any @pytest.mark.asyncio test that pytest-randomly placed later on the same xdist worker failed with RuntimeError: Runner.run() cannot be called from a running event loop — intermittent on roughly half of full-suite runs. Fix is a scope-aware two-layer override: tests/conftest.py declares function-scoped playwright / browser_type / browser / context / page for the unit suite (each unit test that uses page starts a fresh sync_playwright context and releases it on teardown, clearing the loop); tests/e2e/conftest.py carries a counter-override re-declaring playwright / browser_type / browser at session scope so the E2E browser_context (session-scoped, depends on playwright) still resolves against a session-scoped fixture chain. Pytest's nearer-conftest-wins rule guarantees E2E tests see the session-scope version. Deterministic reproduction test (tests/test_playwright_fixture_isolation.py) runs a two-test sandbox subprocess under -p no:randomly to prove both the leak and the fix in isolation.