release: 1.6.3 — EV charge UX (#277) + today-plan ETAs (#298) + cache-bust fix (#301)#303
Merged
Conversation
Reported on 2026-05-29 PROD soak: ``sensor.keba_p30_max_current``
reads stale after SEM commands ``keba.set_current``. Sometimes by
minutes, sometimes returning values SEM never commanded (e.g. 11 A
after SEM commanded 6 A). The dashboard / diagnostics showed the
KEBA's value as if it were SEM's intent — confusing during
debugging.
Root cause is upstream (the KEBA integration's poll vs the
modbus-write register split — out of SEM's control). What SEM can
do is expose its OWN authoritative ``_current_setpoint`` so users
have a clear "what is SEM trying to do" reading alongside the
upstream "what does the charger think it can do."
Fix:
``sensor.py`` — new ``SensorEntityDescription`` per charger:
``sensor.sem_charger_<id>_commanded_current`` (Amperes,
CURRENT device class, MEASUREMENT state class, DIAGNOSTIC
entity category).
``coordinator/coordinator.py`` — wires the publish in the
per-charger result block. Reads ``ev_dev._current_setpoint``
(the authoritative SEM-side cache, updated whenever
``set_current`` is called) and rounds to 1 decimal.
``dashboard/translations.json`` — new ``commanded_current`` key
across all 15 languages.
``dashboard/card/sem-localize.js`` — regenerated (809 keys × 15
langs).
Usage: trust ``sem_charger_<id>_commanded_current`` for "what is
SEM trying to do." Trust the upstream charger sensor
(``keba_p30_max_current`` and analogues) for "what does the charger
think it can do." When the two disagree by more than one cycle, the
upstream sensor is reading stale.
Suite: 2091 / 2091 (no new tests — this is a pure additive
publish with no behavioural change in the control path; the
diagnostic is meant to be read, not gate logic on).
Closes #291.
Completes the EV-budget unification arc by removing the legacy fallbacks that v1.6.0 left side-by-side with the canonical path as a safety net. Carrying two budget formulas alive was exactly the duplication that produced the disagreement bug class in the first place; with three weeks of clean v1.6.0/v1.6.1 PROD soak the fallbacks are dead code, and keeping them invited the next regression. No behavioural changes outside the removals — same upgrade path as any 1.6.x. Removed ------- - flow_calculator.calculate_ev_budget (superseded by canonical) - flow_calculator.calculate_available_power (zero production callers) - flow_calculator.calculate_charging_current(both callers moved to EVControlMixin._watts_to_amps) - ev_control._calculate_solar_ev_budget (74-line legacy fallback; actuator now logs error + emits 0 W if _cycle_ev_budget missing — fail-safe instead of silently masking init bugs with a divergent formula) - coordinator multi-charger distribution legacy fallback (same fail- safe pattern) - sensor._format_charging_state demotion guard (1a9b3c9 cosmetic SOLAR_CHARGING_ACTIVE → ALLOWED downgrade — dead code post- unification, verified across v1.6.0/1.6.1 soak) Tests ----- - Removed unit tests pinning the deleted primitives directly (TestAvailablePower, TestEvBudget, TestEVBudgetSemantics, TestAvailablePowerIncludesBatteryDischarge, TestAvailablePowerInvariants, TestCalculateSolarEvBudget, the budget/current rows from TestEVControlInvariants). - Their physical invariants (non-negative budget, 16 A clamp, battery-discharge inclusion, Zone-3 proportional ramp, measured- discharge override) now ride on calculate_canonical_ev_budget and the scenario harness. - 2042 / 2042 unit tests pass.
…test rewrite Addresses CRITICAL / HIGH / MEDIUM / LOW reviewer findings against the Phase D.2 cleanup commit: - CRITICAL: tests/scenario_harness.py was calling the deleted calculate_ev_budget / calculate_charging_current inside a bare except Exception: pass. After Phase D.2 every scenario assertion passed vacuously because calculated_current fell silently to 0. Rewrote to compute the canonical EVBudget directly and read net_w + current_a from it — scenarios now produce real values (verified: surplus-leak scenario first cycle = 6 A / 4500 W) and regressions fail loudly. - HIGH: tests/test_multi_charger_canonical_budget.py mirrored the pre-D.2 production branch with the legacy fallback. Post-D.2 the branch logs an error and distributes 0 W instead. Rewrote helper to match production; added test_missing_cycle_budget_fails_safe_to_zero pinning the fail-safe + log; added test_legacy_method_attribute_does_not_exist_post_d2 to prevent accidental re-introduction. - MEDIUM: TestEVControlInvariants claimed 16 A clamp coverage but didn't assert it. Added test_canonical_budget_current_a_clamped_to_16 sweeping extreme solar / battery inputs across every non-IDLE strategy (including BATTERY_ASSIST which can blow past the surplus ceiling by design). - MEDIUM: ChargingContext.available_power docstring still referenced FlowCalculator.calculate_ev_budget(). Updated to point at calculate_canonical_ev_budget().net_w. - LOW: ev_control.py fail-safe comment said "Returning 0 W" but the function falls through rather than returning. Reworded. Suite: 2043 / 2043 green (was 2042; +1 from 16 A clamp test).
Pre-fix: sensor.sem_ev_power only updated on the coordinator's 10 s cycle. Observed live on PROD 2026-05-29 as a ~5 kW dashboard gap vs sensor.keba_p30_charging_power for several seconds during EV ramp / drop transients. Root cause: rate mismatch between SEM coordinator (10 s) and the KEBA integration's polling (1-3 s). Post-fix (Option A from #289): the ev_power sensor now ALSO subscribes to its upstream entities via async_track_state_change_event. On any upstream change SEM re-sums and pushes the new value immediately. Resolution mirrors SensorReader._read_ev_power exactly: * multi-charger (≥ 2 ev_chargers): sum every charger's ev_charging_power_sensor * single-charger / legacy: top-level ev_power_sensor with fallback to ev_chargers[0].ev_charging_power_sensor The energy-balance derivations (home_consumption_power, sankey flows) stay on cycle granularity and self-heal on the next tick — the brief 1-cycle disagreement between sem_ev_power and home_consumption_power is acceptable per the #289 analysis and corrects without intervention. 11 new unit tests covering: - Subscription resolution: single / multi / no-upstream / fallback - Single-charger callback: pushes upstream value; unavailable upstream doesn't clear last value; unparseable state is skipped silently - Multi-charger callback: sums across upstreams; partial unavailable sums the valid ones - Cleanup: removing entity calls the unsub; safe when never subscribed Suite: 2054 / 2054 green.
Phase A of the EV charge UX consolidation arc. Purely additive: the
new selector persists user intent but the coordinator strategy
machine still reads the legacy four-toggle state (ev_charging_mode +
night_charging + tariff_optimized + smart_night_charging). Behaviour
is identical to v1.6.2 until Phase B flips authority to the new
field.
What changes
------------
1. New per-charger entity: select.sem_charger_<id>_charge_mode with
5 options (solar_only / solar_plus_cheap / min_plus_solar /
always_max / off). The solar_plus_cheap option is dynamically
hidden when no dynamic tariff is configured (Q1 resolution).
2. async_migrate_entry v4→v5 derives charge_mode per charger from
the legacy toggle state. The mapping table and Q1–Q4 decisions
are documented in docs/plans/2026-05-30_ev_charge_mode_consolidation.md.
3. SEMCoordinator._effective_charge_mode_for(charger_cfg) helper —
the single read-point Phase B will switch authority on. Returns
stored value if present, derives from legacy toggles otherwise.
No production caller reads it yet (Phase A safety property).
4. consts/ev_charge_modes.py: shared EV_CHARGE_MODES dict +
DEFAULT_EV_CHARGE_MODE. Both select.py and coordinator.py import
from here so the mode-name set has one source of truth.
5. async_migrate_entry refactor: each step now reads from and writes
back to threaded accumulated_{data,options} accumulators instead
of entry.{data,options} directly. Fixes a pre-existing bug
exposed by the v5 step — sequential steps were re-reading the
original entry options, losing prior step mutations on test
harnesses (real HA mutated entry between steps and masked it).
6. config_flow.py: VERSION = 4 → VERSION = 5.
7. 15 languages × translations/*.json: entity.select.charge_mode
labels + state translations.
Design questions resolved
-------------------------
- Q1 (no-tariff): hide solar_plus_cheap entirely.
- Q2 (battery assist in min_plus_solar): always available, no toggle.
- Q3 (smart night): implicit ON for night-charging modes, toggle hidden.
- Q4 (default for new installs): min_plus_solar.
Tests
-----
- 26 new tests in test_277_charge_mode_phase_a.py covering: derive
decision tree (10), dynamic options Q1 (4 + current_option clamp),
full migration loop (5), _effective_charge_mode_for equivalence
(5), translations completeness (1), Phase B authority hint.
- test_per_charger_seed_migration: version assertions bumped 4 → 5.
- Suite: 2079 / 2079 passing.
(Pre-existing clock-sensitive flake test_reconciliation_when_
integrated_close_to_hardware filed separately as #294 — fails near
the 07:00 local sunrise-based daily-reset boundary. Not caused by
this change.)
…to fixed time Pre-fix the test depended on wall-clock and broke on two fronts when a CI run crossed a meter-day boundary (07:00 local): 1. The test seeded the accumulator with one ``today`` value computed from ``get_current_meter_day_sunrise_based()`` while ``calculate_energy`` looked it up using ``_ev_reset_day`` (which post-#279 calls ``get_current_meter_day_offset_based("07:00")``). When those two boundary methods returned different dates, the lookup missed. 2. Even if (1) was patched, ``_check_rollover`` runs every cycle and prunes ``ev_daily_sun_*`` accumulators not matching the real-clock today or yesterday. A seeded key for a different date got silently deleted, defeating any in-test mock of the date computation. Fix: ``@freeze_time("2026-05-15 12:00:00")`` — noon, far from any boundary. Both reads see the same date by construction; rollover matches the seeded key. Use ``calc._ev_reset_day(None)`` to compute the seed key with the exact method ``calculate_energy`` will use internally, eliminating the path-divergence risk entirely. Caught by PR #295 CI run that fired at 06:24 UTC — was filed as #294 and resolved here as part of unblocking that PR.
…se-a #277 Phase A: Charge mode selector + v4→v5 migration
Phase B of the EV charge UX consolidation. Routes the night gate
and smart-night-sizing reads through the named charge_mode (the
clean-mapping parts of the legacy four-toggle state) while leaving
``_tariff_optimized_for`` and ``ev_charging_mode`` on the legacy
switches until Phase C rewrites the strategy machine.
Re-scoped from the full plan after a test sweep flagged that two
legacy combinations have no clean home in the 5-mode taxonomy:
- ``minpv + tariff_optimized=on`` — would silently lose the
daytime Min-PV tariff pause if collapsed.
- ``pv`` vs ``minpv`` as the daytime strategy gate — distinct
zone semantics that flattening would lose.
Authority transferred (mode is now source of truth)
---------------------------------------------------
1. ``charging_control.ChargingStateMachine._read_night_enabled_raw``
— was reading per-charger ``switch.sem_charger_<id>_night_charging``;
now derives from the named mode (any charger whose mode permits
night → gate ON). Pre-EV / no-chargers fallback to the legacy
global ``switch.sem_night_charging`` preserved for backward-compat
(Phase C removes).
2. ``coordinator._smart_night_charging_enabled`` — same pattern for
the per-charger smart-night switch.
3. Inline per-charger night gate at the multi-charger surplus
distribution loop (``coordinator.py:1085+``).
4. Inline EV preview gate at the "today's plan" daytime fallback
(``coordinator.py:1640+``).
Authority deferred (still legacy)
---------------------------------
- ``ev_control._tariff_optimized_for`` — unchanged behaviour;
inline docstring explains the deferral.
- ``coordinator._determine_charging_strategy`` ``charging_mode``
read — unchanged; inline comment explains the deferral.
New infrastructure
------------------
- ``consts/ev_charge_modes.py`` gains ``effective_charge_mode_for``
as a module-level free function so ``ChargingStateMachine`` (a
separate class, not a coordinator mixin) shares the same resolver.
Plus the predicate sets ``MODE_NIGHT_ALLOWED`` / ``MODE_USES_TARIFF``
/ ``MODE_USES_SMART_NIGHT`` and the ``MODE_TO_LEGACY_CHARGING_MODE``
adapter (the latter unused in Phase B — Phase C consumes it).
- ``SEMCoordinator`` gains ``_mode_allows_night_charging``,
``_mode_uses_tariff``, ``_mode_uses_smart_night`` helpers.
``_legacy_charging_mode_for`` was prototyped here but removed
before commit — Phase C will add it alongside its call site so
the strategy-machine rewrite ships atomically.
Derivation fix (Phase A bug)
----------------------------
Phase A's ``_derive_charge_mode`` only mapped ``mode == "auto" and
tariff`` to ``solar_plus_cheap``, silently dropping the tariff
signal for ``mode in (pv, self_consumption) and tariff_on``. Phase B
fixes the derivation forward (both ``_derive_charge_mode`` in
``__init__.py`` and ``effective_charge_mode_for`` in
``consts/ev_charge_modes.py``) AND ships a narrow v5→v6 fix-up
migration that re-derives any charger whose stored mode is
``min_plus_solar`` AND legacy mode is ``pv/auto/self_consumption``
AND tariff switch is ON. Explicitly-picked ``min_plus_solar``
without tariff is untouched.
Tests
-----
- New: ``tests/test_277_charge_mode_phase_b.py``, 56 tests covering
the 5-mode predicate truth tables, end-to-end mode-driven night
gate via real ``ChargingStateMachine``, ``_smart_night_charging_
enabled`` mode-driven path, runtime↔migration equivalence (now
including the pv+tariff fix and the documented deferred
minpv+tariff combinations), v5→v6 fix-up coverage (correct,
preserve, multi-charger, no-chargers-no-crash), and source-of-
truth invariants on the constant sets.
- Updated: ``test_night_charging_debounce.py`` scaffold — the
pre-Phase-B "one is_state call per cycle" assumption no longer
holds (resolver makes 1-2 reads). Filter by entity_id so only
``_night_charging`` lookups consume the sequence; tariff lookups
return the factory default. Debounce semantics tested unchanged.
- Updated: version assertions in seed-migration + Phase A tests
bumped to 6.
Plus dead-code cleanup
----------------------
- Removed dead ``ENTITY_SMART_NIGHT_CHARGING`` import in
``coordinator.py`` (never referenced; cleanup folded in since
Phase B is the PR that retired the last switch read using it).
Suite: 2136 / 2136 passing (was 2080 on develop, +56).
Phase B safety contract: existing PROD installs see ZERO behaviour
change. The new infrastructure is in place for Phase C to swap the
remaining two switches.
…se-b #277 Phase B: mode-driven authority transfer (re-scoped) + v5→v6 fix-up
…selector
Replaces the legacy four-toggle stack in the per-charger card section
(``ev_grid_charging`` switch + nested ``ev_tariff_mode`` switch + the
dead ``modeLabels`` computation) with a single named ``Charge mode``
selector + a per-mode help line that explains what the selected mode
actually does.
Options come from the HA select entity itself, so ``solar_plus_cheap``
is auto-hidden when no dynamic tariff is configured (Q1). The cheap-
window hint (``Next cheap window …``) is now only shown when the
selected mode actually uses the tariff windows — pre-fix, the line
appeared whenever a tariff was on, even in modes that ignore it.
What changed
------------
- ``dashboard/card/src/cards/sem-ev-status-card.js``:
- ``_renderChargerSection``: dropped ``ctToggle`` + nightOnLive +
tariffOnLive + the nested tariff sub-row; added the ``Charge
mode`` selector with 5-mode labels + per-mode hints; gated the
cheap-window indicator on ``chargeMode === 'solar_plus_cheap'``.
- ``render``: removed the dead ``modeLabels`` / ``modeEntity`` lookup
of the legacy ``ev_charging_mode`` select — its data was never
rendered.
- New ``.ct-mode-select`` CSS modeled on the existing ``.ct-unit``
select so the two selectors visually rhyme.
- ``dashboard/translations.json``: added 11 new keys × 15 languages —
``charge_mode``, the 5 mode labels, and the 5 per-mode hints.
- ``dashboard/card/sem-localize.js``: regenerated from translations.json.
- ``dashboard/card/dist/sem-cards.js``: rebuilt bundle.
Verified on HA-TEST
-------------------
- Phase A+B migration ran cleanly: ``charge_mode = "min_plus_solar"``
(derived from legacy ``pv + night=on + tariff=off``).
- Entity ``select.sem_charger_ev_charger_charge_mode`` registered
with options ``["solar_only","min_plus_solar","always_max","off"]``
— ``solar_plus_cheap`` correctly hidden (no dynamic tariff configured).
- Schema bumped to v6.
- Card bundle md5-verified on HA-TEST.
This is the user-visible payoff of the #277 arc. The strategy machine
still reads the legacy ``ev_charging_mode`` + ``_tariff_optimized_for``
switches under the hood (deferred to Phase C per the Phase B PR
narrative); Phase B.2's UI just exposes the named modes to the user.
…se-b2 #277 Phase B.2: per-charger card UI uses the new Charge mode selector
Adds three new ETA rows to the ``sensor.sem_charging_state.attributes .today_plan`` timeline, surfaced on the Home tab card: - Battery full at HH:MM (when battery is currently charging) - Battery reaches floor (when battery is currently discharging) - EV reaches target (when EV is currently charging) Each ETA is a best-guess from the CURRENT power rate (same vintage as the existing ev_min_reached row). Steady-state at the present rate is a useful approximation for "when does this finish?" What changed ------------ - ``coordinator/today_plan.py``: three new KIND_* constants, three new parameters (``battery_full_eta`` / ``battery_empty_eta`` / ``ev_target_eta`` + ``ev_target_kwh`` substitution value). - ``coordinator/coordinator.py`` ``today_plan`` call site: computes each ETA from current PowerReadings (battery_power / battery_soc / ev_power / daily_ev_per_charger / target ceilings). Suppresses when |rate| < 200 W or SOC is already at the boundary. - ``dashboard/card/src/cards/sem-today-plan-card.js``: KIND→icon+color mapping for the three new kinds (pink for battery-charging, teal for battery-discharging, soft-green for EV target). - ``dashboard/translations.json``: ``plan_battery_full`` / ``plan_battery_empty`` / ``plan_ev_target_reached`` × 15 languages. - ``dashboard/card/sem-localize.js``: regenerated. - ``dashboard/card/dist/sem-cards.js``: rebuilt bundle. Tests ----- - ``tests/test_today_plan.py``: new ``TestLiveETARows`` class, 8 tests covering each ETA in isolation, simultaneous emission, past ETAs suppressed, outside-horizon suppressed, optional values substitution, and the default no-op when no ETAs are passed. - Suite: 2144 / 2144 green (was 2136 on develop, +8 #298 tests). Closes #298
feat(#298): Today's plan — battery + EV target ETA rows
…oval
The architectural finish of the EV charge UX consolidation arc. The
named ``charge_mode`` is now the sole authority for EV charging
intent — the strategy machine dispatches on it directly,
``_tariff_optimized_for`` derives from it, and the legacy per-charger
switches + ``ev_charging_mode`` select are removed.
Maintainer-resolved Phase C design questions:
- Q1 (``min_plus_solar`` strategy mapping): zone-adaptive (pure ``pv``
fallthrough). Matches Q4 "zero behaviour change for new installs"
since the factory default was always ``pv + night=on``.
- Q2 (legacy switches): full removal in this PR (no read-only mirror
intermediate step).
Mode → strategy dispatch
------------------------
- ``always_max`` → ``("now", …)``
- ``off`` → ``("idle", …)``
- ``solar_only`` → ``_self_consumption_strategy`` (strict surplus)
- ``solar_plus_cheap`` → tariff pause check (was on ``minpv``); then zone
- ``min_plus_solar`` → fall through to pure zone logic
What's gone
-----------
- Per-charger ``switch.sem_charger_<id>_(night_charging|smart_night_
charging|tariff_optimized)`` — entities + creation code + #255
reconciliation block + ``smart_night_charging`` from the options
flow.
- Per-charger ``select.sem_charger_<id>_ev_charging_mode`` —
entity registration + entity-registry orphan cleanup added.
- Strategy machine's legacy ``self.config.get("ev_charging_mode")``
read.
- ``_tariff_optimized_for``'s ``switch.sem_charger_<id>_
tariff_optimized`` read.
- ``effective_charge_mode_for``'s legacy-toggle derivation fallback —
missing ``charge_mode`` now returns ``DEFAULT_EV_CHARGE_MODE``.
- ``charging_control._read_night_enabled_raw``'s pre-EV
``switch.sem_night_charging`` global fallback.
- ``coordinator._mirror_primary_charger_to_global``'s
``ev_charging_mode`` mirror entry.
- Orphaned ``strings.json`` + 15-language entity translations for
the removed switches + select.
v6 → v7 migration
-----------------
Drops ``ev_charging_mode`` from each charger and from the top-level
config. Logs INFO per charger with the removed value.
⚠️ Behavioural change for explicit-``minpv`` legacy users
---------------------------------------------------------
Pre-Phase-C: ``ev_charging_mode=minpv`` users had forced grid-Min
during the day + solar to Max. Post-Phase-C: their ``min_plus_solar``
mode goes through zone-adaptive logic; the Min guarantee comes from
NIGHT top-up only. Documented in the CHANGELOG migration note.
Test sweep
----------
- ``test_277_charge_mode_phase_a.py``: legacy-fallback equivalence
tests removed (the fallback path is gone); defensive defaults
tests updated.
- ``test_277_charge_mode_phase_b.py``: ``TestRuntimeMigrationEquivalence``
renamed to ``TestDeriveChargeModeAllLegacyCombinations`` and
scoped to migration only. ``test_no_chargers_falls_back_to_
global_switch`` rewritten to assert no-fallback (the switch is
gone in this PR). ``test_pre_migration_legacy_falls_back_via_
derivation`` renamed + comment fixed (was misleading after the
derivation fallback removal — flagged in review).
- ``test_night_gate_per_charger.py``: replaced with empty-module
comment (coverage moved to phase_b tests).
- ``test_night_charging_debounce.py``: scaffold rewritten to mutate
``charge_mode`` rather than ``hass.states.is_state`` per cycle —
exercises the production resolver path directly.
- ``test_charging_control.py`` / ``test_dual_state_machine.py``:
fixtures get ``ev_chargers`` with ``charge_mode``; night-disabled
tests flip mode to ``solar_only``.
- ``test_ev_deadline_tariff.py``: ``cfg`` dicts get
``charge_mode: "solar_plus_cheap"`` for tariff tests; legacy
``minpv`` tests rewritten as ``solar_plus_cheap`` /
``min_plus_solar`` cases.
- ``test_switch.py`` / ``test_integration.py``: assert
``observer_mode`` is the only global switch left.
- ``test_per_charger_seed_migration.py``: renamed test; asserts
``ev_charging_mode`` is dropped by v7.
- ``test_scenario.py``: ``test_now_mode_overrides`` →
``test_always_max_mode_overrides``.
Suite: 2130 / 2130 passing.
Reviewer pass before commit. Four findings:
- MEDIUM (test name + comment) — addressed in this commit.
- LOW (dead config-flow field) — removed.
- LOW (dead mirror entry) — removed.
- NIT (CHANGELOG) — added [Unreleased] v1.7.0 candidate section.
Refs #277 (Phase C of 3 — architectural finish).
…se-c #277 Phase C: strategy machine reads charge_mode + legacy switch removal
…298) Bump version + finalize CHANGELOG header for the v1.7.0 release that ships the complete #277 arc (Phases A + B + B.2 + C) plus the #298 today-plan ETA rows. Highlights ---------- - New per-charger ``Charge mode`` selector (5 modes) replaces the four-toggle soup (#277 Phase B.2) - Strategy machine + tariff path now read ``charge_mode`` directly (#277 Phase C) - Today's plan shows live ETAs for battery full / battery floor / EV target reached (#298) - v4 → v7 migration chain (4 sequential schema bumps) seeds the new field, fixes a Phase A derivation bug, then retires the legacy ``ev_charging_mode`` config key - 3 legacy per-charger switches + 1 legacy per-charger select removed from the entity registry⚠️ Behavioural change for explicit-``minpv`` legacy users — see the v1.7.0 CHANGELOG section's "Behavioural change" note. Suite: 2130 / 2130 tests passing.
The legacy generate_dashboard service path used `int(time.time())` as
the `?v=` cache-bust for sem-localize.js and the other root-level card
files registered in `.storage/lovelace_resources`. The timestamp only
moved on `generate_dashboard` service calls — never on plain rsync
deploys — so when v1.7.0 rsynced a fresh sem-localize.js with the new
charge_mode_* keys, the registered URL stayed put and the browser kept
serving the stale cached file. The new EV charge-mode selector then
rendered raw key strings (`charge_mode`, `charge_mode_min_plus_so…`,
`charge_mode_hint_min_plus_solar`) instead of localized labels.
Fix: replace the timestamp with `{manifest_version}-{sha1(content)[:8]}`,
mirroring the format `_async_register_frontend_resources` already uses
for `dist/sem-cards.js`. Both registration paths now produce identical
URLs for the same file content, so any deploy that changes the file
content auto-flips the URL on the next service call and the browser
cache-misses through to the fresh copy.
Extracted as the module-level `_content_hash_cache_bust` helper so the
behaviour is directly testable. Added 6 regression tests covering
URL format, change-on-edit, stability-when-unchanged, the nested
bundle path, missing-file fallback, and a source guard that the
`int(time.time())` anti-pattern cannot silently return.
Co-authored-by: belinea4071 <belinea4071@users.noreply.github.com>
…hot sweep Renumbers the in-flight release from 1.7.0 to 1.6.3. The entity removals (`night_charging`, `smart_night_charging`, `tariff_optimized` switches; `ev_charging_mode` select) are treated as a patch by the maintainer's policy — the named `Charge mode` selector carries the same intent so existing automations only need an entity rename, not a behaviour rewrite. Doc sweep for the renamed entities so first-touch users land on the new model: - README — Charging Modes section rewritten around the 5 named modes (Solar only / Solar + cheapest hours / Min + Solar / Always (max) / Off). Cheap-tariff example automation switched from `switch.turn_on` on `night_charging` to `select.select_option` on `charge_mode`. - EV_CHARGING_LOGIC.md — v1.6.3 banner at top with the new mode table; the legacy toggle-architecture body kept as historical reference for migrating users. Embeds the refreshed EV-tab screenshot. - QUICK_START / SETUP_GUIDE / MULTI_DEVICE_GUIDE — entity tables now show `select.sem_charger_<id>_charge_mode` instead of the removed switches. - images/ — sem_ev_tab.png and sem_home_tab.png recaptured from HA-PROD on v1.6.3; the EV card screenshot is the canonical visual for the new Charge mode selector (label, value, hint line all localized — visual proof the #301 cache-bust fix lands). - CHANGELOG — the prose reference to "v1.7.0 Phase C makes min_plus_solar zone-adaptive" reads as "v1.6.3 Phase C" so the in-section narration matches the header. - tests/test_dashboard_generator.py — version literal in the `_content_hash_cache_bust` fixtures bumped to 1.6.3 for consistency (behaviour unchanged; the helper accepts any version string). manifest.json: 1.7.0 -> 1.6.3. 2136 / 2136 tests passing.
Pre-deploy review for v1.6.3 (#301 PR series) flagged duplicated ``_tariff_pause_warned = False`` calls in the daytime tariff-pause path — once inside the EXPENSIVE branch, once after it. The review's read was that the EXPENSIVE-branch clear was a bug; closer reading shows both clears are inside the ``try``, so both fire only on a successful ``get_price_level()``. The actual invariant is ``successful read ⇒ flag cleared``, regardless of which price level came back. Refactor pulls the clear above the price-level branch so the invariant is local to one line. Behaviour-identical to the prior factoring but no longer reads like a price-level bug to future reviewers. New ``TestTariffPauseWarningFlap`` covers the contract: - one warning per provider outage, even across many ticks - successful read with EXPENSIVE price clears the flag - successful read with CHEAP price clears the flag - a recovery + fresh outage re-arms and re-fires Follow-up cleanups deferred to #304 (select.py orphan-removal gap for previously-removed chargers) and #305 (dead _auto_mode_strategy + min_pv mapper branch). 2140 / 2140 tests passing (4 new).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ev_charging_mode×night_charging×smart_night_charging×tariff_optimized) with one named per-chargerCharge modeselector (Solar only / Solar + cheapest hours / Min + Solar / Always (max) / Off). Three-phase migration arc (A → B → B.2 → C); legacy entities removed.generate_dashboardwas usingint(time.time())for?v=, so a plain rsync deploy that updatedsem-localize.jsleft the registered URL unchanged and browsers served the stale cached file. Symptom: new charge-mode selector rendered raw translation keys. Fix replaces it with content-hash + manifest version, matching the format_async_register_frontend_resourcesalready uses for the Lit bundle.Version
Maintainer numbered this as a patch (1.6.3) rather than a minor (1.7.0) — the named
Charge modeselector carries the same intent as the four removed switches, so existing automations need an entity rename rather than a behaviour rewrite. Manifest bumped accordingly, CHANGELOG and in-narration references updated.Doc / screenshot sweep
switch.turn_ontoselect.select_optiononcharge_mode.docs/images/sem_ev_tab.pngandsem_home_tab.pngrecaptured from HA-PROD on v1.6.3. The EV-tab screenshot is the canonical visual proof that the Stale Lovelace cache-bust serves old sem-localize.js after deploy #301 fix lands (label, value, hint all localized).Behavioural change
The
min_plus_solardefault is now zone-adaptive during the day — the Min guarantee comes from night charging top-up only, not from forced grid pull at noon. Strict "Min from grid at all times" users should pickalways_max. CHANGELOG carries the full migration notes (v4 → v5 → v6 → v7).Verification
__init__.pymatches local on both boxes.generate_dashboardcall on PROD refreshed every Lovelace resource URL to the content-hash format (verified). EV card screenshot confirms the v1.6.3 selector renders with localized labels.Test plan
TestContentHashCacheBustcovers URL format, change-on-edit, stability-when-unchanged, nesteddist/path, missing-file fallback, and an anti-pattern source guardAuto-closes #277, #298, #301 on merge.