Skip to content

Feature/favorites view, grid view changes, panel-offline#228

Merged
cayossarian merged 54 commits into
mainfrom
feature/favorites-view
Apr 17, 2026
Merged

Feature/favorites view, grid view changes, panel-offline#228
cayossarian merged 54 commits into
mainfrom
feature/favorites-view

Conversation

@cayossarian
Copy link
Copy Markdown
Member

@cayossarian cayossarian commented Apr 16, 2026

Grid view changes for multiple columns, code review changes

Panel off-line fixes, bump span-panel-api to 2.6.1

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).
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py with 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-provided entity_id into 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.js string tab.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:

Comment thread CHANGELOG.md Outdated
Comment thread custom_components/span_panel/translations/fr.json Outdated
…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.
@cayossarian cayossarian requested a review from Copilot April 16, 2026 18:46
@cayossarian cayossarian changed the title Feature/favorites view, grid view changes, review changes Feature/favorites view, grid view changes, panel-offline Apr 16, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 25 changed files in this pull request and generated no new comments.

…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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 37 changed files in this pull request and generated 3 comments.

Comment thread custom_components/span_panel/services.py Outdated
Comment thread pyproject.toml
Comment thread CHANGELOG.md Outdated
…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.
@cayossarian cayossarian merged commit b19d8f9 into main Apr 17, 2026
7 checks passed
@cayossarian cayossarian deleted the feature/favorites-view branch April 17, 2026 03:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants