Split esphome state property decorators (#124332)

This commit is contained in:
J. Nick Koston 2024-08-25 06:48:17 -10:00 committed by GitHub
parent ebc49d938a
commit 41b129e990
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 49 additions and 36 deletions

View file

@ -58,6 +58,7 @@ from homeassistant.core import callback
from .entity import ( from .entity import (
EsphomeEntity, EsphomeEntity,
convert_api_error_ha_error, convert_api_error_ha_error,
esphome_float_state_property,
esphome_state_property, esphome_state_property,
platform_async_setup_entry, platform_async_setup_entry,
) )
@ -227,7 +228,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
return _SWING_MODES.from_esphome(self._state.swing_mode) return _SWING_MODES.from_esphome(self._state.swing_mode)
@property @property
@esphome_state_property @esphome_float_state_property
def current_temperature(self) -> float | None: def current_temperature(self) -> float | None:
"""Return the current temperature.""" """Return the current temperature."""
return self._state.current_temperature return self._state.current_temperature
@ -241,19 +242,19 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
return round(self._state.current_humidity) return round(self._state.current_humidity)
@property @property
@esphome_state_property @esphome_float_state_property
def target_temperature(self) -> float | None: def target_temperature(self) -> float | None:
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._state.target_temperature return self._state.target_temperature
@property @property
@esphome_state_property @esphome_float_state_property
def target_temperature_low(self) -> float | None: def target_temperature_low(self) -> float | None:
"""Return the lowbound target temperature we try to reach.""" """Return the lowbound target temperature we try to reach."""
return self._state.target_temperature_low return self._state.target_temperature_low
@property @property
@esphome_state_property @esphome_float_state_property
def target_temperature_high(self) -> float | None: def target_temperature_high(self) -> float | None:
"""Return the highbound target temperature we try to reach.""" """Return the highbound target temperature we try to reach."""
return self._state.target_temperature_high return self._state.target_temperature_high

View file

@ -118,20 +118,35 @@ def esphome_state_property[_R, _EntityT: EsphomeEntity[Any, Any]](
) -> Callable[[_EntityT], _R | None]: ) -> Callable[[_EntityT], _R | None]:
"""Wrap a state property of an esphome entity. """Wrap a state property of an esphome entity.
This checks if the state object in the entity is set, and This checks if the state object in the entity is set
prevents writing NAN values to the Home Assistant state machine. and returns None if it is not set.
""" """
@functools.wraps(func) @functools.wraps(func)
def _wrapper(self: _EntityT) -> _R | None: def _wrapper(self: _EntityT) -> _R | None:
return func(self) if self._has_state else None
return _wrapper
def esphome_float_state_property[_EntityT: EsphomeEntity[Any, Any]](
func: Callable[[_EntityT], float | None],
) -> Callable[[_EntityT], float | None]:
"""Wrap a state property of an esphome entity that returns a float.
This checks if the state object in the entity is set, and returns
None if its not set. If also prevents writing NAN values to the
Home Assistant state machine.
"""
@functools.wraps(func)
def _wrapper(self: _EntityT) -> float | None:
if not self._has_state: if not self._has_state:
return None return None
val = func(self) val = func(self)
if isinstance(val, float) and not math.isfinite(val):
# Home Assistant doesn't use NaN or inf values in state machine # Home Assistant doesn't use NaN or inf values in state machine
# (not JSON serializable) # (not JSON serializable)
return None return None if val is None or not math.isfinite(val) else val
return val
return _wrapper return _wrapper

View file

@ -29,6 +29,7 @@ from homeassistant.core import callback
from .entity import ( from .entity import (
EsphomeEntity, EsphomeEntity,
convert_api_error_ha_error, convert_api_error_ha_error,
esphome_float_state_property,
esphome_state_property, esphome_state_property,
platform_async_setup_entry, platform_async_setup_entry,
) )
@ -79,7 +80,7 @@ class EsphomeMediaPlayer(
return self._state.muted return self._state.muted
@property @property
@esphome_state_property @esphome_float_state_property
def volume_level(self) -> float | None: def volume_level(self) -> float | None:
"""Volume level of the media player (0..1).""" """Volume level of the media player (0..1)."""
return self._state.volume return self._state.volume

View file

@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
from functools import partial from functools import partial
import math
from aioesphomeapi import ( from aioesphomeapi import (
EntityInfo, EntityInfo,
@ -19,7 +18,7 @@ from homeassistant.util.enum import try_parse_enum
from .entity import ( from .entity import (
EsphomeEntity, EsphomeEntity,
convert_api_error_ha_error, convert_api_error_ha_error,
esphome_state_property, esphome_float_state_property,
platform_async_setup_entry, platform_async_setup_entry,
) )
from .enum_mapper import EsphomeEnumMapper from .enum_mapper import EsphomeEnumMapper
@ -57,13 +56,11 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity):
self._attr_mode = NumberMode.AUTO self._attr_mode = NumberMode.AUTO
@property @property
@esphome_state_property @esphome_float_state_property
def native_value(self) -> float | None: def native_value(self) -> float | None:
"""Return the state of the entity.""" """Return the state of the entity."""
state = self._state state = self._state
if state.missing_state or not math.isfinite(state.state): return None if state.missing_state else state.state
return None
return state.state
@convert_api_error_ha_error @convert_api_error_ha_error
async def async_set_native_value(self, value: float) -> None: async def async_set_native_value(self, value: float) -> None:

View file

@ -26,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.enum import try_parse_enum from homeassistant.util.enum import try_parse_enum
from .entity import EsphomeEntity, esphome_state_property, platform_async_setup_entry from .entity import EsphomeEntity, platform_async_setup_entry
from .enum_mapper import EsphomeEnumMapper from .enum_mapper import EsphomeEnumMapper
@ -93,15 +93,16 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
self._attr_state_class = _STATE_CLASSES.from_esphome(state_class) self._attr_state_class = _STATE_CLASSES.from_esphome(state_class)
@property @property
@esphome_state_property
def native_value(self) -> datetime | str | None: def native_value(self) -> datetime | str | None:
"""Return the state of the entity.""" """Return the state of the entity."""
state = self._state if not self._has_state or (state := self._state).missing_state:
if state.missing_state or not math.isfinite(state.state):
return None return None
if self._attr_device_class is SensorDeviceClass.TIMESTAMP: state_float = state.state
return dt_util.utc_from_timestamp(state.state) if not math.isfinite(state_float):
return f"{state.state:.{self._static_info.accuracy_decimals}f}" return None
if self.device_class is SensorDeviceClass.TIMESTAMP:
return dt_util.utc_from_timestamp(state_float)
return f"{state_float:.{self._static_info.accuracy_decimals}f}"
class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEntity): class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEntity):
@ -117,17 +118,17 @@ class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEn
) )
@property @property
@esphome_state_property
def native_value(self) -> str | datetime | date | None: def native_value(self) -> str | datetime | date | None:
"""Return the state of the entity.""" """Return the state of the entity."""
state = self._state if not self._has_state or (state := self._state).missing_state:
if state.missing_state:
return None return None
if self._attr_device_class is SensorDeviceClass.TIMESTAMP: state_str = state.state
return dt_util.parse_datetime(state.state) device_class = self.device_class
if device_class is SensorDeviceClass.TIMESTAMP:
return dt_util.parse_datetime(state_str)
if ( if (
self._attr_device_class is SensorDeviceClass.DATE device_class is SensorDeviceClass.DATE
and (value := dt_util.parse_datetime(state.state)) is not None and (value := dt_util.parse_datetime(state_str)) is not None
): ):
return value.date() return value.date()
return state.state return state_str

View file

@ -43,9 +43,7 @@ class EsphomeText(EsphomeEntity[TextInfo, TextState], TextEntity):
def native_value(self) -> str | None: def native_value(self) -> str | None:
"""Return the state of the entity.""" """Return the state of the entity."""
state = self._state state = self._state
if state.missing_state: return None if state.missing_state else state.state
return None
return state.state
@convert_api_error_ha_error @convert_api_error_ha_error
async def async_set_value(self, value: str) -> None: async def async_set_value(self, value: str) -> None: