From 156a88a3a305f9d4d1187ddb7cbfc45ff8fc5ddf Mon Sep 17 00:00:00 2001 From: Antony Kurniawan Date: Mon, 16 Sep 2024 15:49:15 +0700 Subject: [PATCH] Ignore negative derivative when the input is total_increasing (#119141) * if the derivative is negative, ignore it * add option to ignore the negatives or not * add tests for a new ignore negative derivative * add missing description when editing * rename to ignore_negative_derivative to increase clarity of which negative I mean in case in the future we want a ignore_negative_value... * use state_class=total_increasing to ignore the negative derivative * remove ignore negative from the config * add test for total_increasing_reset case * add comments * update test_total_increasing_reset with history tests Also remove the last comment because the test is already clear My existing comment there isn't unique to this unit test but applies to the entire component. The existing web documentation pointing to Wikipedia should suffice. --------- Co-authored-by: Erik Montnemery --- homeassistant/components/derivative/sensor.py | 12 +++++++ tests/components/derivative/test_sensor.py | 36 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 36719b43ccb..be27201bda9 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -10,9 +10,11 @@ from typing import TYPE_CHECKING import voluptuous as vol from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, RestoreSensor, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -238,6 +240,16 @@ class DerivativeSensor(RestoreSensor, SensorEntity): except AssertionError as err: _LOGGER.error("Could not calculate derivative: %s", err) + # For total inreasing sensors, the value is expected to continuously increase. + # A negative derivative for a total increasing sensor likely indicates the + # sensor has been reset. To prevent inaccurate data, discard this sample. + if ( + new_state.attributes.get(ATTR_STATE_CLASS) + == SensorStateClass.TOTAL_INCREASING + and new_derivative < 0 + ): + return + # add latest derivative to the window list self._state_list.append( (old_state.last_updated, new_state.last_updated, new_derivative) diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 3646340cac3..4a4d8519b25 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -8,6 +8,7 @@ from typing import Any from freezegun import freeze_time from homeassistant.components.derivative.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.const import UnitOfPower, UnitOfTime from homeassistant.core import HomeAssistant, State from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -354,6 +355,41 @@ async def test_suffix(hass: HomeAssistant) -> None: assert round(float(state.state), config["sensor"]["round"]) == 0.0 +async def test_total_increasing_reset(hass: HomeAssistant) -> None: + """Test derivative sensor state with total_increasing sensor input where it should ignore the reset value.""" + times = [0, 20, 30, 35, 40, 50, 60] + values = [0, 10, 30, 40, 0, 10, 40] + expected_times = [0, 20, 30, 35, 50, 60] + expected_values = ["0.00", "0.50", "2.00", "2.00", "1.00", "3.00"] + + config, entity_id = await _setup_sensor(hass, {"unit_time": UnitOfTime.SECONDS}) + + base_time = dt_util.utcnow() + actual_times = [] + actual_values = [] + with freeze_time(base_time) as freezer: + for time, value in zip(times, values, strict=False): + current_time = base_time + timedelta(seconds=time) + freezer.move_to(current_time) + hass.states.async_set( + entity_id, + value, + {ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING}, + force_update=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.power") + assert state is not None + + if state.last_reported == current_time: + actual_times.append(time) + actual_values.append(state.state) + + assert actual_times == expected_times + assert actual_values == expected_values + + async def test_device_id( hass: HomeAssistant, entity_registry: er.EntityRegistry,