Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit ec5d55d

Browse files
jesserockzOttoWinter
andauthored
Auto reset on value going back to 0 in ESPHome (home-assistant#53592)
* ESPHome - Auto reset on value going back to 0 * Remove logging lines * Remove useless stuff * Move callback to sensor class Wrap `track_change_event` in `async_on_remove` * Convert to using internal callbacks and RestoreEntity * Don't document fixmes? * Review fixes * Review fixes Co-authored-by: Otto winter <otto@otto-winter.com>
1 parent ef6fc5b commit ec5d55d

File tree

3 files changed

+105
-48
lines changed

3 files changed

+105
-48
lines changed

‎homeassistant/components/esphome/__init__.py‎

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
712712

713713

714714
_InfoT = TypeVar("_InfoT", bound=EntityInfo)
715-
_EntityT = TypeVar("_EntityT", bound="EsphomeBaseEntity[Any,Any]")
715+
_EntityT = TypeVar("_EntityT", bound="EsphomeEntity[Any,Any]")
716716
_StateT = TypeVar("_StateT", bound=EntityState)
717717

718718

@@ -850,7 +850,7 @@ def from_hass(self, value: _ValT) -> _EnumT:
850850
return self._inverse[value]
851851

852852

853-
class EsphomeBaseEntity(Entity, Generic[_InfoT, _StateT]):
853+
class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
854854
"""Define a base esphome entity."""
855855

856856
def __init__(
@@ -882,6 +882,22 @@ async def async_added_to_hass(self) -> None:
882882
)
883883
)
884884

885+
self.async_on_remove(
886+
async_dispatcher_connect(
887+
self.hass,
888+
(
889+
f"esphome_{self._entry_id}"
890+
f"_update_{self._component_key}_{self._key}"
891+
),
892+
self._on_state_update,
893+
)
894+
)
895+
896+
@callback
897+
def _on_state_update(self) -> None:
898+
# Behavior can be changed in child classes
899+
self.async_write_ha_state()
900+
885901
@callback
886902
def _on_device_update(self) -> None:
887903
"""Update the entity state when device info has changed."""
@@ -890,7 +906,7 @@ def _on_device_update(self) -> None:
890906
# Only update the HA state when the full state arrives
891907
# through the next entity state packet.
892908
return
893-
self.async_write_ha_state()
909+
self._on_state_update()
894910

895911
@property
896912
def _entry_id(self) -> str:
@@ -962,23 +978,3 @@ def name(self) -> str:
962978
def should_poll(self) -> bool:
963979
"""Disable polling."""
964980
return False
965-
966-
967-
class EsphomeEntity(EsphomeBaseEntity[_InfoT, _StateT]):
968-
"""Define a generic esphome entity."""
969-
970-
async def async_added_to_hass(self) -> None:
971-
"""Register callbacks."""
972-
973-
await super().async_added_to_hass()
974-
975-
self.async_on_remove(
976-
async_dispatcher_connect(
977-
self.hass,
978-
(
979-
f"esphome_{self._entry_id}"
980-
f"_update_{self._component_key}_{self._key}"
981-
),
982-
self.async_write_ha_state,
983-
)
984-
)

‎homeassistant/components/esphome/camera.py‎

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@
1010
from homeassistant.components import camera
1111
from homeassistant.components.camera import Camera
1212
from homeassistant.config_entries import ConfigEntry
13-
from homeassistant.core import HomeAssistant
14-
from homeassistant.helpers.dispatcher import async_dispatcher_connect
13+
from homeassistant.core import HomeAssistant, callback
1514
from homeassistant.helpers.entity_platform import AddEntitiesCallback
1615

17-
from . import EsphomeBaseEntity, platform_async_setup_entry
16+
from . import EsphomeEntity, platform_async_setup_entry
1817

1918

2019
async def async_setup_entry(
@@ -32,34 +31,22 @@ async def async_setup_entry(
3231
)
3332

3433

35-
class EsphomeCamera(Camera, EsphomeBaseEntity[CameraInfo, CameraState]):
34+
class EsphomeCamera(Camera, EsphomeEntity[CameraInfo, CameraState]):
3635
"""A camera implementation for ESPHome."""
3736

3837
def __init__(self, *args: Any, **kwargs: Any) -> None:
3938
"""Initialize."""
4039
Camera.__init__(self)
41-
EsphomeBaseEntity.__init__(self, *args, **kwargs)
40+
EsphomeEntity.__init__(self, *args, **kwargs)
4241
self._image_cond = asyncio.Condition()
4342

44-
async def async_added_to_hass(self) -> None:
45-
"""Register callbacks."""
46-
47-
await super().async_added_to_hass()
48-
49-
self.async_on_remove(
50-
async_dispatcher_connect(
51-
self.hass,
52-
(
53-
f"esphome_{self._entry_id}"
54-
f"_update_{self._component_key}_{self._key}"
55-
),
56-
self._on_state_update,
57-
)
58-
)
59-
60-
async def _on_state_update(self) -> None:
43+
@callback
44+
def _on_state_update(self) -> None:
6145
"""Notify listeners of new image when update arrives."""
62-
self.async_write_ha_state()
46+
super()._on_state_update()
47+
self.hass.async_create_task(self._on_state_update_coro())
48+
49+
async def _on_state_update_coro(self) -> None:
6350
async with self._image_cond:
6451
self._image_cond.notify_all()
6552

‎homeassistant/components/esphome/sensor.py‎

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Support for esphome sensors."""
22
from __future__ import annotations
33

4+
from contextlib import suppress
5+
from datetime import datetime
46
import math
57
from typing import cast
68

@@ -11,6 +13,7 @@
1113
TextSensorInfo,
1214
TextSensorState,
1315
)
16+
from aioesphomeapi.model import LastResetType
1417
import voluptuous as vol
1518

1619
from homeassistant.components.sensor import (
@@ -20,9 +23,10 @@
2023
SensorEntity,
2124
)
2225
from homeassistant.config_entries import ConfigEntry
23-
from homeassistant.core import HomeAssistant
26+
from homeassistant.core import HomeAssistant, callback
2427
import homeassistant.helpers.config_validation as cv
2528
from homeassistant.helpers.entity_platform import AddEntitiesCallback
29+
from homeassistant.helpers.restore_state import RestoreEntity
2630
from homeassistant.util import dt
2731

2832
from . import (
@@ -71,9 +75,79 @@ async def async_setup_entry(
7175
)
7276

7377

74-
class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
78+
class EsphomeSensor(
79+
EsphomeEntity[SensorInfo, SensorState], SensorEntity, RestoreEntity
80+
):
7581
"""A sensor implementation for esphome."""
7682

83+
_old_state: float | None = None
84+
85+
async def async_added_to_hass(self) -> None:
86+
"""Register callbacks."""
87+
await super().async_added_to_hass()
88+
89+
if self._static_info.last_reset_type != LastResetType.AUTO:
90+
return
91+
92+
# Logic to restore old state for last_reset_type AUTO:
93+
last_state = await self.async_get_last_state()
94+
if last_state is None:
95+
return
96+
97+
if "last_reset" in last_state.attributes:
98+
self._attr_last_reset = dt.as_utc(
99+
datetime.fromisoformat(last_state.attributes["last_reset"])
100+
)
101+
102+
with suppress(ValueError):
103+
self._old_state = float(last_state.state)
104+
105+
@callback
106+
def _on_state_update(self) -> None:
107+
"""Check last_reset when new state arrives."""
108+
if self._static_info.last_reset_type == LastResetType.NEVER:
109+
self._attr_last_reset = dt.utc_from_timestamp(0)
110+
111+
if self._static_info.last_reset_type != LastResetType.AUTO:
112+
super()._on_state_update()
113+
return
114+
115+
# Last reset type AUTO logic for the last_reset property
116+
# In this mode we automatically determine if an accumulator reset
117+
# has taken place.
118+
# We compare the last valid value (_old_state) with the new one.
119+
# If the value has reset to 0 or has significantly reduced we say
120+
# it has reset.
121+
new_state: float | None = None
122+
state = cast("str | None", self.state)
123+
if state is not None:
124+
with suppress(ValueError):
125+
new_state = float(state)
126+
127+
did_reset = False
128+
if new_state is None:
129+
# New state is not a float - we'll detect the reset once we get valid data again
130+
did_reset = False
131+
elif self._old_state is None:
132+
# First measurement we ever got for this sensor, always a reset
133+
did_reset = True
134+
elif new_state == 0:
135+
# don't set reset if both old and new are 0
136+
# we would already have detected the reset on the last state
137+
did_reset = self._old_state != 0
138+
elif new_state < self._old_state:
139+
did_reset = True
140+
141+
# Set last_reset to now if we detected a reset
142+
if did_reset:
143+
self._attr_last_reset = dt.utcnow()
144+
145+
if new_state is not None:
146+
# Only write to old_state if the new one contains actual data
147+
self._old_state = new_state
148+
149+
super()._on_state_update()
150+
77151
@property
78152
def icon(self) -> str | None:
79153
"""Return the icon."""

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /