Compare commits
7 commits
dev
...
gj-2024102
Author | SHA1 | Date | |
---|---|---|---|
|
c7df6d531c | ||
|
88836d3318 | ||
|
b1540a6f02 | ||
|
70068499ac | ||
|
4f7f119e2f | ||
|
2427a52d46 | ||
|
0199697af5 |
2 changed files with 69 additions and 19 deletions
|
@ -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.
|
||||||
|
|
|
@ -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",
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue