Skip to content

Commit 0d853ca

Browse files
committed
Add baseline support for sensors which need to restore state after HA restart: #281
1 parent dddf8f3 commit 0d853ca

6 files changed

Lines changed: 1015 additions & 750 deletions

File tree

custom_components/victron_mqtt/binary_sensor.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44
from typing import Any
5+
from functools import cached_property
56

67
from victron_mqtt import (
78
Device as VictronVenusDevice,
@@ -77,7 +78,7 @@ def _on_update_task(self, value: Any) -> None:
7778
self._attr_is_on = new_val
7879
self.async_write_ha_state()
7980

80-
@property
81+
@cached_property
8182
def is_on(self) -> bool:
8283
"""Return the current state of the binary sensor."""
8384
assert self._attr_is_on is not None

custom_components/victron_mqtt/entity.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""Common code for Victron Venus integration."""
22

33
from abc import abstractmethod
4+
from functools import cached_property
45
import logging
5-
from typing import Any
6+
from typing import TYPE_CHECKING, Any
67

78
from victron_mqtt import (
89
Device as VictronVenusDevice,
@@ -13,6 +14,7 @@
1314

1415
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
1516
from homeassistant.const import EntityCategory, UnitOfTime
17+
from homeassistant.core import HomeAssistant
1618
from homeassistant.helpers.device_registry import DeviceInfo
1719
from homeassistant.helpers.entity import Entity
1820

@@ -25,8 +27,17 @@
2527
_LOGGER = logging.getLogger(__name__)
2628

2729

28-
class VictronBaseEntity(Entity):
29-
"""Implementation of a Victron Venus base entity."""
30+
class VictronBaseEntity(Entity if TYPE_CHECKING else object): # type: ignore[misc]
31+
"""Mixin class for Victron Venus entities.
32+
33+
This is a mixin class that provides common functionality for all Victron
34+
entities. It should be used as the first base class in the inheritance list
35+
together with a specific entity type (SensorEntity, SwitchEntity, etc.)
36+
which provides the actual Entity base class.
37+
38+
The TYPE_CHECKING conditional inheritance is used to satisfy type checkers
39+
while avoiding MRO conflicts at runtime.
40+
"""
3041

3142
def __init__(
3243
self,
@@ -156,7 +167,7 @@ def _map_metric_to_unit_of_measurement(
156167
return UnitOfTime.HOURS
157168
return metric.unit_of_measurement
158169

159-
@property
160-
def device_info(self) -> DeviceInfo:
170+
@cached_property
171+
def device_info(self) -> DeviceInfo | None:
161172
"""Return device information about the sensor."""
162173
return self._device_info

custom_components/victron_mqtt/number.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44
from typing import Any
5+
from functools import cached_property
56

67
from victron_mqtt import (
78
Device as VictronVenusDevice,
@@ -88,7 +89,7 @@ def _on_update_task(self, value: Any) -> None:
8889
self._attr_native_value = value
8990
self.async_write_ha_state()
9091

91-
@property
92+
@cached_property
9293
def native_value(self):
9394
"""Return the current value."""
9495
return self._metric.value

custom_components/victron_mqtt/sensor.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,26 @@
66
"""
77

88
from typing import Any
9+
import logging
910

1011
from victron_mqtt import (
1112
Device as VictronVenusDevice,
13+
FormulaMetric as VictronFormulaMetric,
1214
Metric as VictronVenusMetric,
1315
MetricKind,
1416
)
1517

16-
from homeassistant.components.sensor import SensorEntity
18+
from homeassistant.components.sensor import SensorEntity, SensorStateClass
1719
from homeassistant.config_entries import ConfigEntry
1820
from homeassistant.core import HomeAssistant, callback
1921
from homeassistant.helpers.device_registry import DeviceInfo
2022
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
23+
from homeassistant.helpers.restore_state import RestoreEntity
2124

2225
from .entity import VictronBaseEntity
2326
from .hub import Hub
2427

28+
_LOGGER = logging.getLogger(__name__)
2529

2630
async def async_setup_entry(
2731
hass: HomeAssistant,
@@ -53,8 +57,10 @@ def on_new_metric(
5357
hub.register_new_metric_callback(MetricKind.SENSOR, on_new_metric)
5458

5559

56-
class VictronSensor(VictronBaseEntity, SensorEntity):
60+
class VictronSensor(VictronBaseEntity, SensorEntity, RestoreEntity): # type: ignore[misc]
5761
"""Implementation of a Victron Venus sensor."""
62+
63+
_baseline: float | None = None
5864

5965
def __init__(
6066
self,
@@ -65,15 +71,48 @@ def __init__(
6571
installation_id: str,
6672
) -> None:
6773
"""Initialize the sensor."""
68-
self._attr_native_value = metric.value
6974
super().__init__(
7075
device, metric, device_info, "sensor", simple_naming, installation_id
7176
)
7277

73-
7478
@callback
7579
def _on_update_task(self, value: Any) -> None:
80+
if self._baseline is not None:
81+
value += self._baseline
7682
if self._attr_native_value == value:
7783
return
7884
self._attr_native_value = value
7985
self.async_write_ha_state()
86+
87+
async def async_added_to_hass(self) -> None:
88+
"""Restore persistent state for FormulaMetric energy sensors."""
89+
90+
# Only restore for:
91+
# 1. Total increasing sensors (like cumulative energy)
92+
# 2. FormulaMetrics (calculated values)
93+
should_restore = (
94+
self._attr_state_class in [SensorStateClass.TOTAL_INCREASING, SensorStateClass.TOTAL]
95+
and isinstance(self._metric, VictronFormulaMetric)
96+
)
97+
self._attr_native_value = self._metric.value
98+
if not should_restore:
99+
# Call parent to register update callbacks
100+
await super().async_added_to_hass()
101+
return
102+
103+
last_state = await self.async_get_last_state()
104+
if last_state is not None and last_state.state is not None:
105+
assert isinstance(self._attr_native_value, (int, float)), "sensor with stored baseline value must be numeric"
106+
try:
107+
self._baseline = float(last_state.state)
108+
self._attr_native_value += self._baseline
109+
except ValueError:
110+
_LOGGER.warning(
111+
"Could not restore state for %s: invalid value '%s'",
112+
self.entity_id,
113+
last_state.state,
114+
)
115+
_LOGGER.info("Restored baseline of %.3f for %s", self._baseline, self.entity_id)
116+
# Call parent to register update callbacks
117+
await super().async_added_to_hass()
118+

0 commit comments

Comments
 (0)