Feature/favorites view, grid view changes, panel-offline#228
Merged
Conversation
Adds backend support for the Favorites pseudo-panel in the dashboard.
The integration now persists a per-panel set of favorited circuits and
sub-devices and exposes services to manage them.
- Storage in the existing span_panel_settings Store under a "favorites"
key shaped as {panel_device_id: {circuits: [uuid], sub_devices: [id]}}.
Reads tolerate a legacy single-list shape and migrate it transparently
to circuits-only entries.
- Three new services registered in async_setup:
- get_favorites — response-only, returns the full map
- add_favorite / remove_favorite — take an entity_id (any SPAN sensor)
and resolve server-side to (panel_device_id, kind, target_id) by
walking via_device_id for sub-devices and extracting the 32-char hex
circuit uuid from the entity unique_id otherwise. Validation errors
raise ServiceValidationError with translation keys.
- entity_id-based public API matches set_circuit_threshold's pattern;
internal UUIDs and HA device ids are not part of the user-facing
surface.
- New tests/test_favorites_service.py covers storage round-trip, dedupe,
empty-list cleanup, sibling-setting preservation, legacy shape
migration, and service-handler validation (unknown entity, non-SPAN
platform, sub-device branch, panel-level sensor rejection).
- Strings + translations (en/es/fr/pt/ja) for the new services and
exception keys.
- Sync rebuilt span-card 0.9.3 bundles.
Picks up the sub-device side-panel horizon fix (was writing the override to the wrong panel's manager on multi-panel installs) and documents the Favorites feature, persistent panel header, and related fixes in the unreleased 2.0.6 entry.
Replaces the stale Settings View section (the Settings tab was removed in span-card 0.9.2) with a Favorites View section covering the three heart-toggle locations, the synthetic dropdown entry, the view-specific tab differences, statefulness, cross-panel routing of side-panel edits, and the get_favorites / add_favorite / remove_favorite services. Syncs span-card bundle with the un-favorite-in-view re-render fix.
Replaces the per-circuit and per-sub-device side panels' Favorite ha-switch with a heart icon so it is not visually confused with the breaker relay switch.
Addresses backend findings from the deep code review of the favorites
branch.
Critical:
- ``async_set_favorite`` now holds an ``asyncio.Lock`` around the
load → mutate → save sequence, preventing concurrent dashboard heart
toggles from clobbering each other's writes (HA's Store serializes
individual writes but not the surrounding read-modify-write).
- Legacy ``{panel_id: [uuid]}`` storage shape is now persisted as the
canonical ``{circuits, sub_devices}`` dict on the first write that
touches it; reads still tolerate either shape via
``_normalize_favorites_blob``.
Hardening / clarity:
- Extracted ``extract_circuit_uuid_from_unique_id`` into id_builder.py,
shared by services.py and current_monitor.py. Skips ``parts[0]``
(``span``) and ``parts[1]`` (serial) so a serial that happens to be
32 hex chars cannot shadow the circuit uuid.
- Per-cause ``ServiceValidationError`` translation keys: ``favorite_no_device``,
``favorite_subdevice_no_span_parent``, ``favorite_no_unique_id``,
``favorite_no_circuit_uuid``. Users now see the actual reason their
pick was rejected instead of a generic message.
- ``FavoriteKind = Literal["circuits", "sub_devices"]`` for stricter
typing of the kind argument; resolver return type narrowed to match.
- ``_normalize_favorites_blob`` warns on dropped/malformed entries so
storage corruption is visible during debugging.
- Fixed stale docstring on ``_async_register_favorites_services`` that
still referenced ``circuit_entity_id``.
- Removed unused ``AsyncMock`` import from the test module.
New tests:
- Entity with ``device_id=None`` is rejected.
- Sub-device whose ``via_device_id`` points at a non-SPAN device is
rejected with the new ``favorite_subdevice_no_span_parent`` key.
- Sub-device whose ``via_device_id`` references a missing parent device
is rejected.
- Legacy single-list storage is migrated in place on the next write.
- Concurrent ``async_set_favorite`` calls do not lose writes (lock test).
…eyed cache, refactors)
There was a problem hiding this comment.
Pull request overview
This PR adds cross-panel Favorites support to the SPAN Panel integration, including backend services/storage helpers and documentation updates to describe the new Favorites view and service APIs.
Changes:
- Added favorites persistence helpers in
frontend.pywith normalization/migration of legacy stored favorites and a lock to prevent concurrent-write clobbering. - Registered new HA services (
get_favorites,add_favorite,remove_favorite) that resolve a user-providedentity_idinto a favorites target (circuit UUID or sub-device device id). - Added a comprehensive new test suite validating favorites storage behaviors and service handler resolution/error paths.
Reviewed changes
Copilot reviewed 15 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_favorites_service.py | New test coverage for favorites storage helpers and favorites services resolution/validation paths. |
| frontend.md | Adds documentation for the Favorites view and related services. |
| custom_components/span_panel/translations/pt.json | Adds new translated strings for favorites service errors and service metadata. |
| custom_components/span_panel/translations/ja.json | Adds new translated strings for favorites service errors and service metadata. |
| custom_components/span_panel/translations/fr.json | Adds new translated strings for favorites service errors and service metadata. |
| custom_components/span_panel/translations/es.json | Adds new translated strings for favorites service errors and service metadata. |
| custom_components/span_panel/translations/en.json | Adds new English strings for favorites service errors and service metadata. |
| custom_components/span_panel/strings.json | Adds new source strings for favorites errors and services. |
| custom_components/span_panel/services.yaml | Registers the new services and their entity_id selector metadata. |
| custom_components/span_panel/services.py | Implements and registers favorites services, including entity/device registry resolution. |
| custom_components/span_panel/id_builder.py | Adds helper to extract a circuit UUID from an entity unique_id. |
| custom_components/span_panel/frontend.py | Implements favorites storage read/write with normalization, migration, and concurrency lock. |
| custom_components/span_panel/current_monitor.py | Reuses the new UUID extraction helper for circuit resolution. |
| custom_components/span_panel/init.py | Registers favorites services during integration setup. |
| CHANGELOG.md | Documents the new cross-panel Favorites feature. |
| .gitignore | Adds .mcp.json to ignored files. |
Comments suppressed due to low confidence (1)
frontend.md:76
- The Settings view documentation appears to have been replaced by the Favorites section, but the frontend still exposes a "Settings" tab (see
custom_components/span_panel/frontend/dist/span-panel.jsstringtab.settings). Please restore/update the Settings View section (or add a short section) so the docs reflect all available dashboard tabs.
- **Priority** — Notification priority level (controls iOS interruption level and Android notification channel)
- **Title/Message Templates** — Customizable templates with the following variables:
…line detection Registers register_connection_callback on async_setup_streaming and unregisters on async_shutdown. The new _on_connection_change handler flips panel_offline and calls async_update_listeners() so sensors enter or exit grace-period logic on the edge, instead of waiting for the 60 s fallback poll. _mark_panel_offline now accepts Exception or str so the callback path can pass a concise reason string.
- _on_connection_change now only calls async_update_listeners() when panel_offline actually transitioned. The library contract already suppresses duplicate edges, but this defends against future changes and misbehaving subscribers. - Update pre-existing async_setup_streaming test to also assert the connection callback is registered, closing a coverage gap. - Add a regression test locking the no-op-on-unchanged-state guard.
- SpanCircuitEnergySensorProtocol.energy_offset: add ... body so the Protocol property satisfies its declared float return type (was docstring-only, which implicitly returned None). - tests._create_coordinator: widen client parameter from MagicMock|None to object|None so pre-existing tests using local FakeSpanMqttClient subclasses type-check cleanly. Use cast(SpanMqttClient, ...) at the call site to keep SpanPanelCoordinator's constructor fully typed. - tests: cast unregister_connection back to MagicMock before calling .assert_called_once_with() to resolve the pre-existing mypy narrowing error introduced by the is-identity assertion. Surfaced by pyright while reviewing the broker-connection callback feature; fixed as a drive-by since they block clean type checks of unrelated code.
…ine path Splits the _async_update_data exception handler into an explicit SpanPanelStaleDataError branch (library-declared offline) and a residual broad Exception branch (defense-in-depth for unexpected errors). No behavior change — both branches call _mark_panel_offline and return last-known data. The split makes the two paths clearer for future maintainers.
…flow Registering the custom sidebar panel with `config_panel_domain=DOMAIN` made Home Assistant treat the dashboard as the integration's config panel, so clicking Configure on a SPAN Panel config entry in Settings -> Devices & Services was routing to the dashboard instead of the integration's OptionsFlow. Drop the hook so HA falls back to the standard options-flow UI for Configure while the sidebar panel and dashboard continue to be reachable from the sidebar.
Service registration (B3, B4):
- Add config_entry_id field to 13 services in services.yaml + strings.json
so HA's service UI renders the multi-panel addressing option users need.
- Add set_global_monitoring.enabled field (was in schema but undocumented).
- Add missing exception translation keys monitoring_not_enabled and
graph_horizon_not_available (ServiceValidationError raises them).
- Translate all new keys to es/fr/ja/pt.
Typing (H10, H11, H12 partial, H13, H14, M12, M14, M15):
- Add __all__ to helpers.py so mypy --strict can verify its 30+ re-exports
instead of producing 13 [attr-defined] errors on downstream imports.
- Explicit `as X` re-export of async_apply_panel_registration in __init__.
- Drop stale # type: ignore on EntityCategory, config_flow import, and
_format_notification staticmethod alias.
- Give Store[dict[str, Any]] its generic argument in graph_horizon and
current_monitor.
- Replace ConfigEntry[Any] with SpanPanelConfigEntry in coordinator and
config_flow's _update_v2_entry.
- Replace Any with a NativeSensorValue type alias in grace_period that
mirrors the HA SensorEntity.native_value shape.
- Narrow coerce_grace_period_minutes raw_value annotation from Any.
Behavior / defaults (M1, M4, M5, L1):
- Default ENABLE_ENERGY_DIP_COMPENSATION to True in the options flow
(matches install default; prevents silent disable when editing options).
- alert_dispatcher strftime `%-I` is glibc-only; use `%I` with lstrip("0")
for Windows HA portability.
- handle_offline_grace_period clamps time_since_last_valid at 0 so a
backward clock jump (DST / NTP) doesn't freeze the last value forever.
- Remove dead _last_grace_period tracking in coordinator.
entity_resolver: document why multi- and single-circuit builders default
USE_DEVICE_PREFIX differently (legacy compatibility — not a bug).
If a platform raises during async_unload_platforms, HA keeps the entry loaded and retries. With the previous order, async_shutdown had already stopped MQTT streaming and closed the client, so the still-attached entities held references to a dead client. Now platforms unload first; coordinator shutdown runs only on success.
Three build_*_unique_id functions lower-case the serial; five do not. The inconsistency is real but benign — the entity registry keys on string equality, so every deployed entity still matches itself on restart. Unifying the casing would rewrite what these functions return and orphan every existing switch, binary_sensor, select, BESS, and EVSE entity on any install whose panel serial contains upper-case characters, which in turn breaks user dashboards and statistics history. Comment makes this explicit so a future reviewer doesn't silently "fix" the inconsistency.
…|bool|str Introduce MonitoringValue (int | bool | str) and MonitoringSettings (dict[str, MonitoringValue]) in threshold_evaluator, replacing the dict[str, Any] used for per-point overrides and global settings. Tightens the type on: - CurrentMonitor._circuit_overrides, ._mains_overrides, ._global_settings - set_circuit_override, set_mains_override, set_global_settings, get_global_settings, _is_redundant_override - resolve_thresholds, is_monitoring_disabled alert_dispatcher.dispatch_alert / dispatch_test_alert keep dict[str, Any] at the HA-service boundary — format_notification and notify service data need Any for template lookups and HA service payloads. Also fixes the second glibc-only %-I:%M %p strftime in dispatch_test_alert (first one fixed in Phase 1). No runtime behaviour change. mypy strict clean, 662 tests pass.
strings.json defines entity translation_keys for feedthrough and main- meter consumed / net / produced energy sensors, but icons.json had no matching entries so these sensors fell back to HA's generic sensor icon. Add directional transmission-tower icons for feedthrough and meter-electric variants for the main meter; swap-horizontal for the net sensors. No key changes.
…evice
EVSE feed-circuit sensors are attached to the EVSE sub-device via the
device override in sensor.py, but their unique_id still encodes the
underlying circuit UUID. The previous target resolver treated any entity
with via_device_id as a sub-device favorite, so favoriting a feed-circuit
sensor would store an EVSE-device favorite instead of the circuit UUID.
Try the circuit-UUID branch first: if entry.unique_id embeds a 32-char
circuit UUID, favorite the circuit keyed by the parent panel device.
Fall back to the sub-device branch only when no circuit UUID can be
derived (BESS %, EVSE status metadata sensors).
Also fixes CHANGELOG grammar ("include" → "including the bump").
Mirrors span-card commit 4108f85 — removes the summary strip (gear/slide-to-enable/legend/W-A) from the Favorites Monitoring view so it matches the real-panel Monitoring tab.
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.
Grid view changes for multiple columns, code review changes
Panel off-line fixes, bump span-panel-api to 2.6.1