From e8572889a1b64ac0087aa22f0e260aeb5fe6f4d5 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 22 Mar 2026 23:07:31 -0700 Subject: [PATCH 1/2] Exclude high-churn energy sensor attrs from recorder (#197) Set SpanEnergySensorBase._unrecorded_attributes for grace-period and dip diagnostics plus circuit tabs and nominal voltage. Live entity attributes unchanged; RestoreSensor storage unchanged. Add test and 2.0.4 changelog. Made-with: Cursor --- CHANGELOG.md | 3 +++ custom_components/span_panel/sensor_base.py | 23 ++++++++++++++++++ tests/test_energy_sensor_recorder.py | 26 +++++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 tests/test_energy_sensor_recorder.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a69003..573d389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,9 @@ All notable changes to this project will be documented in this file. - **PV nameplate capacity unit** — Corrected the PV nameplate capacity sensor unit to watts. +- **Recorder database growth** — Energy sensors still expose grace-period and dip-compensation diagnostics, plus circuit `tabs` and `voltage`, on the entity, but + those attributes are no longer written to the recorder, which greatly reduces churn in the `state_attributes` table (#197). + ## [2.0.3] - 3/2026 **Important** 2.0.1 cautions still apply — read those carefully if not already on 2.0.1 BEFORE proceeding: diff --git a/custom_components/span_panel/sensor_base.py b/custom_components/span_panel/sensor_base.py index 9305eb2..1cd6821 100644 --- a/custom_components/span_panel/sensor_base.py +++ b/custom_components/span_panel/sensor_base.py @@ -37,6 +37,23 @@ # Sentinel value to distinguish "never synced" from "circuit name is None" _NAME_UNSET: object = object() +# Keys merged into circuit energy sensors' extra_state_attributes that we omit +# from the recorder. High-churn grace/dip diagnostics dominated DB growth (#197); +# tabs and voltage are usually static but need not be duplicated on every state +# row—they stay on the live entity for Developer tools and automations. +_ENERGY_SENSOR_UNRECORDED_ATTRIBUTES: frozenset[str] = frozenset( + { + "energy_offset", + "grace_period_remaining", + "last_dip_delta", + "last_valid_changed", + "last_valid_state", + "tabs", + "using_grace_period", + "voltage", + } +) + def _parse_numeric_state(state: State | None) -> tuple[float | None, datetime | None]: """Extract a numeric value and naive timestamp from a restored HA state. @@ -445,8 +462,14 @@ class SpanEnergySensorBase[T: SensorEntityDescription, D](SpanSensorBase[T, D], - Grace period tracking for offline scenarios - State restoration across HA restarts via RestoreSensor mixin - Automatic persistence of last_valid_state and last_valid_changed + + High-churn diagnostic attributes are listed in ``extra_state_attributes`` for + the UI but omitted from recorder history via ``_unrecorded_attributes`` so the + database is not flooded with unique attribute blobs on every energy update. """ + _unrecorded_attributes = _ENERGY_SENSOR_UNRECORDED_ATTRIBUTES + def __init__( self, data_coordinator: SpanPanelCoordinator, diff --git a/tests/test_energy_sensor_recorder.py b/tests/test_energy_sensor_recorder.py new file mode 100644 index 0000000..28925b3 --- /dev/null +++ b/tests/test_energy_sensor_recorder.py @@ -0,0 +1,26 @@ +"""Energy sensors exclude volatile attributes from recorder history (#197).""" + +from homeassistant.components.sensor import SensorEntity + +from custom_components.span_panel.sensor_base import SpanEnergySensorBase + + +def test_span_energy_sensor_combined_unrecorded_includes_high_churn_attributes() -> None: + """Recorder must not persist grace-period / dip diagnostic attributes.""" + + combined = getattr(SpanEnergySensorBase, "_Entity__combined_unrecorded_attributes") + for key in ( + "energy_offset", + "grace_period_remaining", + "last_dip_delta", + "last_valid_changed", + "last_valid_state", + "tabs", + "using_grace_period", + "voltage", + ): + assert key in combined, f"missing unrecorded key: {key}" + + assert SensorEntity._entity_component_unrecorded_attributes <= combined, ( + "sensor component exclusions (e.g. options) must remain" + ) From 283fec5cbb0e227abfab331a7190c6b4c5292ae2 Mon Sep 17 00:00:00 2001 From: cayossarian <23534755+cayossarian@users.noreply.github.com> Date: Sun, 22 Mar 2026 23:13:05 -0700 Subject: [PATCH 2/2] docs: clarify unrecorded attrs apply to all Span energy sensors Copilot PR #202: comment wrongly implied circuit-only; SpanEnergySensorBase covers panel and circuit energy entities. Made-with: Cursor --- custom_components/span_panel/sensor_base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/span_panel/sensor_base.py b/custom_components/span_panel/sensor_base.py index 1cd6821..973019f 100644 --- a/custom_components/span_panel/sensor_base.py +++ b/custom_components/span_panel/sensor_base.py @@ -37,10 +37,10 @@ # Sentinel value to distinguish "never synced" from "circuit name is None" _NAME_UNSET: object = object() -# Keys merged into circuit energy sensors' extra_state_attributes that we omit -# from the recorder. High-churn grace/dip diagnostics dominated DB growth (#197); -# tabs and voltage are usually static but need not be duplicated on every state -# row—they stay on the live entity for Developer tools and automations. +# Keys from Span energy sensors' extra_state_attributes that we omit from the recorder +# (SpanEnergySensorBase: panel-wide and circuit energy entities). High-churn grace/dip +# diagnostics dominated DB growth (#197). tabs and voltage are merged in by circuit +# subclasses; they stay on the live entity for Developer tools and automations. _ENERGY_SENSOR_UNRECORDED_ATTRIBUTES: frozenset[str] = frozenset( { "energy_offset",