Allow small dip in total_increasing sensor without detecting a reset (#55153)
This commit is contained in:
parent
8877f37da0
commit
fa9f91325c
4 changed files with 91 additions and 3 deletions
|
@ -11,6 +11,7 @@ from homeassistant.components.sensor import (
|
||||||
STATE_CLASS_TOTAL_INCREASING,
|
STATE_CLASS_TOTAL_INCREASING,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.sensor.recorder import reset_detected
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
ENERGY_KILO_WATT_HOUR,
|
ENERGY_KILO_WATT_HOUR,
|
||||||
|
@ -297,7 +298,7 @@ class EnergyCostSensor(SensorEntity):
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if energy < float(self._last_energy_sensor_state):
|
if reset_detected(energy, float(self._last_energy_sensor_state)):
|
||||||
# Energy meter was reset, reset cost sensor too
|
# Energy meter was reset, reset cost sensor too
|
||||||
self._reset(0)
|
self._reset(0)
|
||||||
# Update with newly incurred cost
|
# Update with newly incurred cost
|
||||||
|
|
|
@ -226,6 +226,11 @@ def _normalize_states(
|
||||||
return DEVICE_CLASS_UNITS[key], fstates
|
return DEVICE_CLASS_UNITS[key], fstates
|
||||||
|
|
||||||
|
|
||||||
|
def reset_detected(state: float, previous_state: float | None) -> bool:
|
||||||
|
"""Test if a total_increasing sensor has been reset."""
|
||||||
|
return previous_state is not None and state < 0.9 * previous_state
|
||||||
|
|
||||||
|
|
||||||
def compile_statistics(
|
def compile_statistics(
|
||||||
hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime
|
hass: HomeAssistant, start: datetime.datetime, end: datetime.datetime
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
@ -308,7 +313,7 @@ def compile_statistics(
|
||||||
fstate,
|
fstate,
|
||||||
)
|
)
|
||||||
elif state_class == STATE_CLASS_TOTAL_INCREASING and (
|
elif state_class == STATE_CLASS_TOTAL_INCREASING and (
|
||||||
old_state is None or (new_state is not None and fstate < new_state)
|
old_state is None or reset_detected(fstate, new_state)
|
||||||
):
|
):
|
||||||
reset = True
|
reset = True
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
|
|
|
@ -216,6 +216,16 @@ async def test_cost_sensor_price_entity(
|
||||||
assert cost_sensor_entity_id in statistics
|
assert cost_sensor_entity_id in statistics
|
||||||
assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 19.0
|
assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 19.0
|
||||||
|
|
||||||
|
# Energy sensor has a small dip, no reset should be detected
|
||||||
|
hass.states.async_set(
|
||||||
|
usage_sensor_entity_id,
|
||||||
|
"14",
|
||||||
|
{ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get(cost_sensor_entity_id)
|
||||||
|
assert state.state == "18.0" # 19 EUR + (14-14.5) kWh * 2 EUR/kWh = 18 EUR
|
||||||
|
|
||||||
# Energy sensor is reset, with initial state at 4kWh, 0 kWh is used as zero-point
|
# Energy sensor is reset, with initial state at 4kWh, 0 kWh is used as zero-point
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
usage_sensor_entity_id,
|
usage_sensor_entity_id,
|
||||||
|
@ -240,7 +250,7 @@ async def test_cost_sensor_price_entity(
|
||||||
await async_wait_recording_done_without_instance(hass)
|
await async_wait_recording_done_without_instance(hass)
|
||||||
statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass)
|
statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass)
|
||||||
assert cost_sensor_entity_id in statistics
|
assert cost_sensor_entity_id in statistics
|
||||||
assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 39.0
|
assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 38.0
|
||||||
|
|
||||||
|
|
||||||
async def test_cost_sensor_handle_wh(hass, hass_storage) -> None:
|
async def test_cost_sensor_handle_wh(hass, hass_storage) -> None:
|
||||||
|
|
|
@ -318,6 +318,78 @@ def test_compile_hourly_sum_statistics_total_increasing(
|
||||||
assert "Error while processing event StatisticsTask" not in caplog.text
|
assert "Error while processing event StatisticsTask" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"device_class,unit,native_unit,factor",
|
||||||
|
[("energy", "kWh", "kWh", 1)],
|
||||||
|
)
|
||||||
|
def test_compile_hourly_sum_statistics_total_increasing_small_dip(
|
||||||
|
hass_recorder, caplog, device_class, unit, native_unit, factor
|
||||||
|
):
|
||||||
|
"""Test small dips in sensor readings do not trigger a reset."""
|
||||||
|
zero = dt_util.utcnow()
|
||||||
|
hass = hass_recorder()
|
||||||
|
recorder = hass.data[DATA_INSTANCE]
|
||||||
|
setup_component(hass, "sensor", {})
|
||||||
|
attributes = {
|
||||||
|
"device_class": device_class,
|
||||||
|
"state_class": "total_increasing",
|
||||||
|
"unit_of_measurement": unit,
|
||||||
|
}
|
||||||
|
seq = [10, 15, 20, 19, 30, 40, 50, 60, 70]
|
||||||
|
|
||||||
|
four, eight, states = record_meter_states(
|
||||||
|
hass, zero, "sensor.test1", attributes, seq
|
||||||
|
)
|
||||||
|
hist = history.get_significant_states(
|
||||||
|
hass, zero - timedelta.resolution, eight + timedelta.resolution
|
||||||
|
)
|
||||||
|
assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"]
|
||||||
|
|
||||||
|
recorder.do_adhoc_statistics(period="hourly", start=zero)
|
||||||
|
wait_recording_done(hass)
|
||||||
|
recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1))
|
||||||
|
wait_recording_done(hass)
|
||||||
|
recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2))
|
||||||
|
wait_recording_done(hass)
|
||||||
|
statistic_ids = list_statistic_ids(hass)
|
||||||
|
assert statistic_ids == [
|
||||||
|
{"statistic_id": "sensor.test1", "unit_of_measurement": native_unit}
|
||||||
|
]
|
||||||
|
stats = statistics_during_period(hass, zero)
|
||||||
|
assert stats == {
|
||||||
|
"sensor.test1": [
|
||||||
|
{
|
||||||
|
"statistic_id": "sensor.test1",
|
||||||
|
"start": process_timestamp_to_utc_isoformat(zero),
|
||||||
|
"max": None,
|
||||||
|
"mean": None,
|
||||||
|
"min": None,
|
||||||
|
"state": approx(factor * seq[2]),
|
||||||
|
"sum": approx(factor * 10.0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"statistic_id": "sensor.test1",
|
||||||
|
"start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)),
|
||||||
|
"max": None,
|
||||||
|
"mean": None,
|
||||||
|
"min": None,
|
||||||
|
"state": approx(factor * seq[5]),
|
||||||
|
"sum": approx(factor * 30.0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"statistic_id": "sensor.test1",
|
||||||
|
"start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)),
|
||||||
|
"max": None,
|
||||||
|
"mean": None,
|
||||||
|
"min": None,
|
||||||
|
"state": approx(factor * seq[8]),
|
||||||
|
"sum": approx(factor * 60.0),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
assert "Error while processing event StatisticsTask" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog):
|
def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog):
|
||||||
"""Test compiling hourly statistics."""
|
"""Test compiling hourly statistics."""
|
||||||
zero = dt_util.utcnow()
|
zero = dt_util.utcnow()
|
||||||
|
|
Loading…
Add table
Reference in a new issue