diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a690038..573d389c 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 9305eb2a..973019f6 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 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", + "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 00000000..28925b32 --- /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" + )