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=trueandcharge_phase=scheduledon the backing sensor. The cards hardcodedsensor.foxess_smart_operationsas theoperations_entitydefault, but HA derives entity_ids from the translated friendly name at entity-creation time — in DE the real entity issensor.foxess_intelligente_steuerung, in FRsensor.foxess_operations_intelligentes, etc. The integration already exposesfoxess_control/entity_mapfor 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: explicitoperations_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 directthis._config.operations_entityreads). - Charge-phase sensor oscillated rapidly between
charginganddeferredduring an active session: observed 2026-04-27 on a live charge session (11:00–13:59 window) wheresensor.foxess_smart_operationsflipped 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 tochargingas inputs jittered. Root cause:is_effectively_charging()insmart_battery/sensor_base.pyindependently recomputedcalculate_deferred_start()from live coordinator data on every ~5s WS refresh;net_consumption_kwjitter of ±1 kW (appliances cycling, solar flicker) swung the computeddeferred_startby 10–30 minutes tick-to-tick, crossing thenow >= deferredthreshold in both directions. SoC jitter of 0.1% (BMS reporting granularity / interpolation noise) had the same effect. Fix: the listener now commits itscalculate_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) drivesis_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.dataretains 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 rawNonesurfaces 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_cardand the taper-cardpage.evaluateinjection intests/e2e/test_ui.pynow 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 fornetworkidle, and retry with a freshly resolved locator/context. Mirrors the existing_find_cardretry pattern so all gallery-screenshot and card-injection tests inherit the recovery in one place; unrelated errors (genuineTimeout, etc.) propagate unchanged. Each helper has deterministic stub-based unit coverage for the retry, re-resolve, and error-passthrough contracts. pytest-playwrightevent-loop leak under-n auto(C-031 flake eliminated): pytest-playwright's session-scopedplaywrightfixture callssync_playwright().start(), which keeps a greenlet-backed asyncio event loop pinned as "running" on the main thread until end-of-session. Any@pytest.mark.asynciotest thatpytest-randomlyplaced later on the same xdist worker failed withRuntimeError: 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.pydeclares function-scopedplaywright/browser_type/browser/context/pagefor the unit suite (each unit test that usespagestarts a freshsync_playwrightcontext and releases it on teardown, clearing the loop);tests/e2e/conftest.pycarries a counter-override re-declaringplaywright/browser_type/browserat session scope so the E2Ebrowser_context(session-scoped, depends onplaywright) 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:randomlyto prove both the leak and the fix in isolation.