Compare commits

...
Sign in to create a new pull request.

7 commits

Author SHA1 Message Date
G Johansson
c7df6d531c Fix tests 2024-11-03 18:23:12 +00:00
G Johansson
88836d3318 Fix 2024-11-03 16:36:14 +00:00
G Johansson
b1540a6f02 age_coverage_ratio = 0 as default 2024-10-29 18:35:00 +00:00
G Johansson
70068499ac Fix test 2024-10-28 22:06:53 +00:00
G Johansson
4f7f119e2f Add test 2024-10-28 22:02:02 +00:00
G Johansson
2427a52d46 Mod _update_extra_state_attributes 2024-10-28 21:46:23 +00:00
G Johansson
0199697af5 Use shorthand attribute for extra state attributes in statistics 2024-10-28 21:45:34 +00:00
2 changed files with 69 additions and 19 deletions

View file

@ -364,7 +364,7 @@ class StatisticsSensor(SensorEntity):
self.states: deque[float | bool] = deque(maxlen=self._samples_max_buffer_size) self.states: deque[float | bool] = deque(maxlen=self._samples_max_buffer_size)
self.ages: deque[datetime] = deque(maxlen=self._samples_max_buffer_size) self.ages: deque[datetime] = deque(maxlen=self._samples_max_buffer_size)
self.attributes: dict[str, StateType] = {} self._attr_extra_state_attributes = {}
self._state_characteristic_fn: Callable[[], float | int | datetime | None] = ( self._state_characteristic_fn: Callable[[], float | int | datetime | None] = (
self._callable_characteristic_fn(self._state_characteristic) self._callable_characteristic_fn(self._state_characteristic)
@ -462,10 +462,10 @@ class StatisticsSensor(SensorEntity):
# Here we make a copy the current value, which is okay. # Here we make a copy the current value, which is okay.
self._attr_available = new_state.state != STATE_UNAVAILABLE self._attr_available = new_state.state != STATE_UNAVAILABLE
if new_state.state == STATE_UNAVAILABLE: if new_state.state == STATE_UNAVAILABLE:
self.attributes[STAT_SOURCE_VALUE_VALID] = None self._attr_extra_state_attributes[STAT_SOURCE_VALUE_VALID] = None
return return
if new_state.state in (STATE_UNKNOWN, None, ""): if new_state.state in (STATE_UNKNOWN, None, ""):
self.attributes[STAT_SOURCE_VALUE_VALID] = False self._attr_extra_state_attributes[STAT_SOURCE_VALUE_VALID] = False
return return
try: try:
@ -475,9 +475,9 @@ class StatisticsSensor(SensorEntity):
else: else:
self.states.append(float(new_state.state)) self.states.append(float(new_state.state))
self.ages.append(new_state.last_reported) self.ages.append(new_state.last_reported)
self.attributes[STAT_SOURCE_VALUE_VALID] = True self._attr_extra_state_attributes[STAT_SOURCE_VALUE_VALID] = True
except ValueError: except ValueError:
self.attributes[STAT_SOURCE_VALUE_VALID] = False self._attr_extra_state_attributes[STAT_SOURCE_VALUE_VALID] = False
_LOGGER.error( _LOGGER.error(
"%s: parsing error. Expected number or binary state, but received '%s'", "%s: parsing error. Expected number or binary state, but received '%s'",
self.entity_id, self.entity_id,
@ -584,13 +584,6 @@ class StatisticsSensor(SensorEntity):
return None return None
return SensorStateClass.MEASUREMENT return SensorStateClass.MEASUREMENT
@property
def extra_state_attributes(self) -> dict[str, StateType] | None:
"""Return the state attributes of the sensor."""
return {
key: value for key, value in self.attributes.items() if value is not None
}
def _purge_old_states(self, max_age: timedelta) -> None: def _purge_old_states(self, max_age: timedelta) -> None:
"""Remove states which are older than a given age.""" """Remove states which are older than a given age."""
now = dt_util.utcnow() now = dt_util.utcnow()
@ -657,7 +650,7 @@ class StatisticsSensor(SensorEntity):
if self._samples_max_age is not None: if self._samples_max_age is not None:
self._purge_old_states(self._samples_max_age) self._purge_old_states(self._samples_max_age)
self._update_attributes() self._update_extra_state_attributes()
self._update_value() self._update_value()
# If max_age is set, ensure to update again after the defined interval. # If max_age is set, ensure to update again after the defined interval.
@ -738,22 +731,22 @@ class StatisticsSensor(SensorEntity):
self.async_write_ha_state() self.async_write_ha_state()
_LOGGER.debug("%s: initializing from database completed", self.entity_id) _LOGGER.debug("%s: initializing from database completed", self.entity_id)
def _update_attributes(self) -> None: def _update_extra_state_attributes(self) -> None:
"""Calculate and update the various attributes.""" """Calculate and update the various attributes."""
if self._samples_max_buffer_size is not None: if self._samples_max_buffer_size is not None:
self.attributes[STAT_BUFFER_USAGE_RATIO] = round( self._attr_extra_state_attributes[STAT_BUFFER_USAGE_RATIO] = round(
len(self.states) / self._samples_max_buffer_size, 2 len(self.states) / self._samples_max_buffer_size, 2
) )
if self._samples_max_age is not None: if self._samples_max_age is not None:
if len(self.states) >= 1: if len(self.states) >= 1:
self.attributes[STAT_AGE_COVERAGE_RATIO] = round( self._attr_extra_state_attributes[STAT_AGE_COVERAGE_RATIO] = round(
(self.ages[-1] - self.ages[0]).total_seconds() (self.ages[-1] - self.ages[0]).total_seconds()
/ self._samples_max_age.total_seconds(), / self._samples_max_age.total_seconds(),
2, 2,
) )
else: else:
self.attributes[STAT_AGE_COVERAGE_RATIO] = None self._attr_extra_state_attributes[STAT_AGE_COVERAGE_RATIO] = 0
def _update_value(self) -> None: def _update_value(self) -> None:
"""Front to call the right statistical characteristics functions. """Front to call the right statistical characteristics functions.

View file

@ -118,7 +118,6 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant) -> None:
assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2)
assert state.attributes.get("source_value_valid") is True assert state.attributes.get("source_value_valid") is True
assert "age_coverage_ratio" not in state.attributes assert "age_coverage_ratio" not in state.attributes
# Source sensor turns unavailable, then available with valid value, # Source sensor turns unavailable, then available with valid value,
# statistics sensor should follow # statistics sensor should follow
state = hass.states.get("sensor.test") state = hass.states.get("sensor.test")
@ -576,7 +575,7 @@ async def test_age_limit_expiry(hass: HomeAssistant) -> None:
assert state is not None assert state is not None
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
assert state.attributes.get("buffer_usage_ratio") == round(0 / 20, 2) assert state.attributes.get("buffer_usage_ratio") == round(0 / 20, 2)
assert state.attributes.get("age_coverage_ratio") is None assert state.attributes.get("age_coverage_ratio") == 0
async def test_age_limit_expiry_with_keep_last_sample(hass: HomeAssistant) -> None: async def test_age_limit_expiry_with_keep_last_sample(hass: HomeAssistant) -> None:
@ -2032,3 +2031,61 @@ async def test_not_valid_device_class(hass: HomeAssistant) -> None:
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None
assert state.attributes.get(ATTR_DEVICE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) is None
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
async def test_attributes_remains(recorder_mock: Recorder, hass: HomeAssistant) -> None:
"""Test attributes are always present."""
for value in VALUES_NUMERIC:
hass.states.async_set(
"sensor.test_monitored",
str(value),
{ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS},
)
await hass.async_block_till_done()
await async_wait_recording_done(hass)
current_time = dt_util.utcnow()
with freeze_time(current_time) as freezer:
assert await async_setup_component(
hass,
"sensor",
{
"sensor": [
{
"platform": "statistics",
"name": "test",
"entity_id": "sensor.test_monitored",
"state_characteristic": "mean",
"max_age": {"seconds": 10},
},
]
},
)
await hass.async_block_till_done()
state = hass.states.get("sensor.test")
assert state is not None
assert state.state == str(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2))
assert state.attributes == {
"age_coverage_ratio": 0.0,
"friendly_name": "test",
"icon": "mdi:calculator",
"source_value_valid": True,
"state_class": SensorStateClass.MEASUREMENT,
"unit_of_measurement": "°C",
}
freezer.move_to(current_time + timedelta(minutes=1))
async_fire_time_changed(hass)
state = hass.states.get("sensor.test")
assert state is not None
assert state.state == STATE_UNKNOWN
assert state.attributes == {
"age_coverage_ratio": 0,
"friendly_name": "test",
"icon": "mdi:calculator",
"source_value_valid": True,
"state_class": SensorStateClass.MEASUREMENT,
"unit_of_measurement": "°C",
}