From 9f18bf63b45e4df97afa3fe8e8af5aba4125ce60 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 17 Aug 2021 13:33:58 +0200 Subject: [PATCH 01/14] Remove STATE_CLASS_TOTAL --- homeassistant/components/sensor/__init__.py | 22 --------------------- homeassistant/components/sensor/recorder.py | 20 +++---------------- 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 950af5a1375c0..7cefb26c87c1b 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -91,14 +91,11 @@ # The state represents a measurement in present time STATE_CLASS_MEASUREMENT: Final = "measurement" -# The state represents a total amount, e.g. a value of a stock portfolio -STATE_CLASS_TOTAL: Final = "total" # The state represents a monotonically increasing total, e.g. an amount of consumed gas STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing" STATE_CLASSES: Final[list[str]] = [ STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, ] @@ -178,25 +175,6 @@ def capability_attributes(self) -> Mapping[str, Any] | None: def state_attributes(self) -> dict[str, Any] | None: """Return state attributes.""" if last_reset := self.last_reset: - if ( - last_reset is not None - and self.state_class == STATE_CLASS_MEASUREMENT - and not self._last_reset_reported - ): - self._last_reset_reported = True - report_issue = self._suggest_report_issue() - _LOGGER.warning( - "Entity %s (%s) with state_class %s has set last_reset. Setting " - "last_reset for entities with state_class other than 'total' is " - "deprecated and will be removed from Home Assistant Core 2021.10. " - "Please update your configuration if state_class is manually " - "configured, otherwise %s", - self.entity_id, - type(self), - self.state_class, - report_issue, - ) - return {ATTR_LAST_RESET: last_reset.isoformat()} return None diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 48f80bab5c2aa..323de9761a584 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -17,7 +17,6 @@ DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, - STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, STATE_CLASSES, ) @@ -53,22 +52,16 @@ _LOGGER = logging.getLogger(__name__) DEVICE_CLASS_OR_UNIT_STATISTICS = { - STATE_CLASS_TOTAL: { - DEVICE_CLASS_ENERGY: {"sum"}, - DEVICE_CLASS_GAS: {"sum"}, - DEVICE_CLASS_MONETARY: {"sum"}, - }, STATE_CLASS_MEASUREMENT: { DEVICE_CLASS_BATTERY: {"mean", "min", "max"}, + DEVICE_CLASS_ENERGY: {"sum"}, + DEVICE_CLASS_GAS: {"sum"}, DEVICE_CLASS_HUMIDITY: {"mean", "min", "max"}, + DEVICE_CLASS_MONETARY: {"sum"}, DEVICE_CLASS_POWER: {"mean", "min", "max"}, DEVICE_CLASS_PRESSURE: {"mean", "min", "max"}, DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, PERCENTAGE: {"mean", "min", "max"}, - # Deprecated, support will be removed in Home Assistant 2021.10 - DEVICE_CLASS_ENERGY: {"sum"}, - DEVICE_CLASS_GAS: {"sum"}, - DEVICE_CLASS_MONETARY: {"sum"}, }, STATE_CLASS_TOTAL_INCREASING: { DEVICE_CLASS_ENERGY: {"sum"}, @@ -291,13 +284,6 @@ def compile_statistics( for fstate, state in fstates: - # Deprecated, will be removed in Home Assistant 2021.10 - if ( - "last_reset" not in state.attributes - and state_class == STATE_CLASS_MEASUREMENT - ): - continue - reset = False if ( state_class != STATE_CLASS_TOTAL_INCREASING From 44b5537bd2dd34ffc1c28740e359ed3b79f72503 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 17 Aug 2021 14:17:08 +0200 Subject: [PATCH 02/14] Update mill sensor --- homeassistant/components/mill/sensor.py | 26 ++----------------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 5241f95abdb16..ce7704ad1bed2 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -2,11 +2,10 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ENERGY_KILO_WATT_HOUR -from homeassistant.util import dt as dt_util from .const import CONSUMPTION_TODAY, CONSUMPTION_YEAR, DOMAIN, MANUFACTURER @@ -29,7 +28,7 @@ class MillHeaterEnergySensor(SensorEntity): _attr_device_class = DEVICE_CLASS_ENERGY _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR - _attr_state_class = STATE_CLASS_TOTAL + _attr_state_class = STATE_CLASS_TOTAL_INCREASING def __init__(self, heater, mill_data_connection, sensor_type): """Initialize the sensor.""" @@ -45,16 +44,6 @@ def __init__(self, heater, mill_data_connection, sensor_type): "manufacturer": MANUFACTURER, "model": f"generation {1 if heater.is_gen1 else 2}", } - if self._sensor_type == CONSUMPTION_TODAY: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) - ) - elif self._sensor_type == CONSUMPTION_YEAR: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace( - month=1, day=1, hour=0, minute=0, second=0, microsecond=0 - ) - ) async def async_update(self): """Retrieve latest state.""" @@ -71,15 +60,4 @@ async def async_update(self): self._attr_native_value = _state return - if self.state is not None and _state < self.state: - if self._sensor_type == CONSUMPTION_TODAY: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) - ) - elif self._sensor_type == CONSUMPTION_YEAR: - self._attr_last_reset = dt_util.as_utc( - dt_util.now().replace( - month=1, day=1, hour=0, minute=0, second=0, microsecond=0 - ) - ) self._attr_native_value = _state From 4ec6f94dc3ff7ac63527b77529b62b836661da46 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 17 Aug 2021 15:28:41 +0200 Subject: [PATCH 03/14] Update tests --- homeassistant/components/sensor/recorder.py | 6 --- tests/components/sensor/test_init.py | 22 ----------- tests/components/sensor/test_recorder.py | 44 ++++++++++++++++++--- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 323de9761a584..2aa9c6b3e61c2 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -313,12 +313,6 @@ def compile_statistics( else: new_state = fstate - # Deprecated, will be removed in Home Assistant 2021.10 - if last_reset is None and state_class == STATE_CLASS_MEASUREMENT: - # No valid updates - result.pop(entity_id) - continue - if new_state is None or old_state is None: # No valid updates result.pop(entity_id) diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 793bcaf4f999a..f09cd4894898b 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1,7 +1,6 @@ """The test for sensor device automation.""" from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.setup import async_setup_component -from homeassistant.util import dt as dt_util async def test_deprecated_temperature_conversion( @@ -29,24 +28,3 @@ async def test_deprecated_temperature_conversion( "your configuration if device_class is manually configured, otherwise report it " "to the custom component author." ) in caplog.text - - -async def test_deprecated_last_reset(hass, caplog, enable_custom_integrations): - """Test warning on deprecated last reset.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( - name="Test", state_class="measurement", last_reset=dt_util.utc_from_timestamp(0) - ) - - assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) - await hass.async_block_till_done() - - assert ( - "Entity sensor.test () " - "with state_class measurement has set last_reset. Setting last_reset for " - "entities with state_class other than 'total' is deprecated and will be " - "removed from Home Assistant Core 2021.10. Please update your configuration if " - "state_class is manually configured, otherwise report it to the custom " - "component author." - ) in caplog.text diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index d4dee872823da..182ed0a058214 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -154,7 +154,6 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes assert "Error while processing event StatisticsTask" not in caplog.text -@pytest.mark.parametrize("state_class", ["measurement", "total"]) @pytest.mark.parametrize( "device_class,unit,native_unit,factor", [ @@ -167,7 +166,7 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes ], ) def test_compile_hourly_sum_statistics_amount( - hass_recorder, caplog, state_class, device_class, unit, native_unit, factor + hass_recorder, caplog, device_class, unit, native_unit, factor ): """Test compiling hourly statistics.""" zero = dt_util.utcnow() @@ -176,7 +175,7 @@ def test_compile_hourly_sum_statistics_amount( setup_component(hass, "sensor", {}) attributes = { "device_class": device_class, - "state_class": state_class, + "state_class": "measurement", "unit_of_measurement": unit, "last_reset": None, } @@ -249,7 +248,7 @@ def test_compile_hourly_sum_statistics_amount( ("gas", "ft³", "m³", 0.0283168466), ], ) -def test_compile_hourly_sum_statistics_total_no_reset( +def test_compile_hourly_sum_statistics_no_reset( hass_recorder, caplog, device_class, unit, native_unit, factor ): """Test compiling hourly statistics.""" @@ -259,7 +258,7 @@ def test_compile_hourly_sum_statistics_total_no_reset( setup_component(hass, "sensor", {}) attributes = { "device_class": device_class, - "state_class": "total", + "state_class": "measurement", "unit_of_measurement": unit, } seq = [10, 15, 20, 10, 30, 40, 50, 60, 70] @@ -416,8 +415,8 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): sns3_attr = {} sns4_attr = { "device_class": "energy", - "state_class": "measurement", "unit_of_measurement": "kWh", + "last_reset": None, } seq1 = [10, 15, 20, 10, 30, 40, 50, 60, 70] seq2 = [110, 120, 130, 0, 30, 45, 55, 65, 75] @@ -531,6 +530,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): {"statistic_id": "sensor.test1", "unit_of_measurement": "kWh"}, {"statistic_id": "sensor.test2", "unit_of_measurement": "kWh"}, {"statistic_id": "sensor.test3", "unit_of_measurement": "kWh"}, + {"statistic_id": "sensor.test4", "unit_of_measurement": "kWh"}, ] stats = statistics_during_period(hass, zero) assert stats == { @@ -630,6 +630,38 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "sum": approx(70.0 / 1000), }, ], + "sensor.test4": [ + { + "statistic_id": "sensor.test4", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(5.0), + "sum": approx(5.0), + }, + { + "statistic_id": "sensor.test4", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(50.0), + "sum": approx(50.0), + }, + { + "statistic_id": "sensor.test4", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(90.0), + "sum": approx(90.0), + }, + ], } assert "Error while processing event StatisticsTask" not in caplog.text From c38e191abd2c9fa2eb1f61e584aaa51b6ce08ac2 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 17 Aug 2021 18:10:29 +0200 Subject: [PATCH 04/14] Kill last_reset --- homeassistant/components/recorder/models.py | 2 -- .../components/recorder/statistics.py | 2 -- homeassistant/components/sensor/__init__.py | 25 ++----------------- homeassistant/components/sensor/recorder.py | 20 ++------------- 4 files changed, 4 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index ff64deb60cdbd..fe75ba1cb50ca 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -218,7 +218,6 @@ class StatisticData(TypedDict, total=False): mean: float min: float max: float - last_reset: datetime | None state: float sum: float @@ -242,7 +241,6 @@ class Statistics(Base): # type: ignore mean = Column(Float()) min = Column(Float()) max = Column(Float()) - last_reset = Column(DATETIME_TYPE) state = Column(Float()) sum = Column(Float()) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index b91e4d160dffd..6017f050419a9 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -43,7 +43,6 @@ Statistics.mean, Statistics.min, Statistics.max, - Statistics.last_reset, Statistics.state, Statistics.sum, ] @@ -375,7 +374,6 @@ def _sorted_statistics_to_dict( "mean": convert(db_state.mean, units), "min": convert(db_state.min, units), "max": convert(db_state.max, units), - "last_reset": _process_timestamp_to_utc_isoformat(db_state.last_reset), "state": convert(db_state.state, units), "sum": convert(db_state.sum, units), } diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 7cefb26c87c1b..693b0664bb5de 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -4,9 +4,9 @@ from collections.abc import Mapping from contextlib import suppress from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import timedelta import logging -from typing import Any, Final, cast, final +from typing import Any, Final, cast import voluptuous as vol @@ -51,7 +51,6 @@ _LOGGER: Final = logging.getLogger(__name__) -ATTR_LAST_RESET: Final = "last_reset" ATTR_STATE_CLASS: Final = "state_class" DOMAIN: Final = "sensor" @@ -129,7 +128,6 @@ class SensorEntityDescription(EntityDescription): """A class that describes sensor entities.""" state_class: str | None = None - last_reset: datetime | None = None native_unit_of_measurement: str | None = None @@ -137,7 +135,6 @@ class SensorEntity(Entity): """Base class for sensor entities.""" entity_description: SensorEntityDescription - _attr_last_reset: datetime | None _attr_native_unit_of_measurement: str | None _attr_native_value: StateType = None _attr_state_class: str | None @@ -153,15 +150,6 @@ def state_class(self) -> str | None: return self.entity_description.state_class return None - @property - def last_reset(self) -> datetime | None: - """Return the time when the sensor was last reset, if any.""" - if hasattr(self, "_attr_last_reset"): - return self._attr_last_reset - if hasattr(self, "entity_description"): - return self.entity_description.last_reset - return None - @property def capability_attributes(self) -> Mapping[str, Any] | None: """Return the capability attributes.""" @@ -170,15 +158,6 @@ def capability_attributes(self) -> Mapping[str, Any] | None: return None - @final - @property - def state_attributes(self) -> dict[str, Any] | None: - """Return state attributes.""" - if last_reset := self.last_reset: - return {ATTR_LAST_RESET: last_reset.isoformat()} - - return None - @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 2aa9c6b3e61c2..0adefabd13c2a 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -42,20 +42,17 @@ VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant, State -import homeassistant.util.dt as dt_util import homeassistant.util.pressure as pressure_util import homeassistant.util.temperature as temperature_util import homeassistant.util.volume as volume_util -from . import ATTR_LAST_RESET, DOMAIN +from . import DOMAIN _LOGGER = logging.getLogger(__name__) DEVICE_CLASS_OR_UNIT_STATISTICS = { STATE_CLASS_MEASUREMENT: { DEVICE_CLASS_BATTERY: {"mean", "min", "max"}, - DEVICE_CLASS_ENERGY: {"sum"}, - DEVICE_CLASS_GAS: {"sum"}, DEVICE_CLASS_HUMIDITY: {"mean", "min", "max"}, DEVICE_CLASS_MONETARY: {"sum"}, DEVICE_CLASS_POWER: {"mean", "min", "max"}, @@ -272,26 +269,18 @@ def compile_statistics( stat["mean"] = _time_weighted_average(fstates, start, end) if "sum" in wanted_statistics: - last_reset = old_last_reset = None new_state = old_state = None _sum = 0 last_stats = statistics.get_last_statistics(hass, 1, entity_id) if entity_id in last_stats: # We have compiled history for this sensor before, use that as a starting point - last_reset = old_last_reset = last_stats[entity_id][0]["last_reset"] new_state = old_state = last_stats[entity_id][0]["state"] _sum = last_stats[entity_id][0]["sum"] for fstate, state in fstates: reset = False - if ( - state_class != STATE_CLASS_TOTAL_INCREASING - and (last_reset := state.attributes.get("last_reset")) - != old_last_reset - ): - reset = True - elif old_state is None and last_reset is None: + if old_state is None: reset = True elif state_class == STATE_CLASS_TOTAL_INCREASING and ( old_state is None or (new_state is not None and fstate < new_state) @@ -320,8 +309,6 @@ def compile_statistics( # Update the sum with the last state _sum += new_state - old_state - if last_reset is not None: - stat["last_reset"] = dt_util.parse_datetime(last_reset) stat["sum"] = _sum stat["state"] = new_state @@ -345,9 +332,6 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - state = hass.states.get(entity_id) assert state - if "sum" in provided_statistics and ATTR_LAST_RESET not in state.attributes: - continue - native_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if key not in UNIT_CONVERSIONS: From 4b94aece33a36936097d840a79923ab988b9a236 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 17 Aug 2021 18:14:44 +0200 Subject: [PATCH 05/14] Return ATTR_LAST_RESET to utility_meter --- homeassistant/components/utility_meter/sensor.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 1ff201aaceb33..ed4af4228ba68 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -5,11 +5,7 @@ import voluptuous as vol -from homeassistant.components.sensor import ( - ATTR_LAST_RESET, - STATE_CLASS_MEASUREMENT, - SensorEntity, -) +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, @@ -58,6 +54,7 @@ ATTR_STATUS = "status" ATTR_PERIOD = "meter_period" ATTR_LAST_PERIOD = "last_period" +ATTR_LAST_RESET = "last_reset" ATTR_TARIFF = "tariff" DEVICE_CLASS_MAP = { @@ -352,6 +349,7 @@ def extra_state_attributes(self): ATTR_SOURCE_ID: self._sensor_source_id, ATTR_STATUS: PAUSED if self._collecting is None else COLLECTING, ATTR_LAST_PERIOD: self._last_period, + ATTR_LAST_RESET: self._last_reset, } if self._period is not None: state_attr[ATTR_PERIOD] = self._period @@ -363,8 +361,3 @@ def extra_state_attributes(self): def icon(self): """Return the icon to use in the frontend, if any.""" return ICON - - @property - def last_reset(self): - """Return the time when the sensor was last reset.""" - return self._last_reset From 95d2325ac159c26992e2fd2ad94089ef42837d4f Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 17 Aug 2021 18:29:38 +0200 Subject: [PATCH 06/14] Update energy cost sensor --- homeassistant/components/energy/sensor.py | 14 ++++---------- homeassistant/components/sensor/recorder.py | 1 + 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index fd36611acafd3..cd017f098f870 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -6,9 +6,8 @@ from typing import Any, Final, Literal, TypeVar, cast from homeassistant.components.sensor import ( - ATTR_LAST_RESET, DEVICE_CLASS_MONETARY, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( @@ -21,7 +20,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.dt as dt_util from .const import DOMAIN from .data import EnergyManager, async_get_manager @@ -203,7 +201,7 @@ def __init__( f"{config[adapter.entity_energy_key]}_{adapter.entity_id_suffix}" ) self._attr_device_class = DEVICE_CLASS_MONETARY - self._attr_state_class = STATE_CLASS_MEASUREMENT + self._attr_state_class = STATE_CLASS_TOTAL_INCREASING self._config = config self._last_energy_sensor_state: State | None = None self._cur_value = 0.0 @@ -212,7 +210,6 @@ def _reset(self, energy_state: State) -> None: """Reset the cost sensor.""" self._attr_native_value = 0.0 self._cur_value = 0.0 - self._attr_last_reset = dt_util.utcnow() self._last_energy_sensor_state = energy_state self.async_write_ha_state() @@ -223,7 +220,7 @@ def _update_cost(self) -> None: cast(str, self._config[self._adapter.entity_energy_key]) ) - if energy_state is None or ATTR_LAST_RESET not in energy_state.attributes: + if energy_state is None: return try: @@ -280,10 +277,7 @@ def _update_cost(self) -> None: ) return - if ( - energy_state.attributes[ATTR_LAST_RESET] - != self._last_energy_sensor_state.attributes[ATTR_LAST_RESET] - ): + if energy < float(self._last_energy_sensor_state.state): # Energy meter was reset, reset cost sensor too self._reset(energy_state) else: diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 0adefabd13c2a..2a1d82305f0ff 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -63,6 +63,7 @@ STATE_CLASS_TOTAL_INCREASING: { DEVICE_CLASS_ENERGY: {"sum"}, DEVICE_CLASS_GAS: {"sum"}, + DEVICE_CLASS_MONETARY: {"sum"}, }, } From 65bc900d681a48922d2a78cd5a5679a81e77f266 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 17 Aug 2021 20:41:59 +0200 Subject: [PATCH 07/14] Restore last_reset for backwards compatibility --- homeassistant/components/sensor/__init__.py | 25 +++- homeassistant/components/sensor/recorder.py | 21 ++- tests/components/sensor/test_recorder.py | 156 ++------------------ 3 files changed, 51 insertions(+), 151 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 693b0664bb5de..18de56e4a5163 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -4,9 +4,9 @@ from collections.abc import Mapping from contextlib import suppress from dataclasses import dataclass -from datetime import timedelta +from datetime import datetime, timedelta import logging -from typing import Any, Final, cast +from typing import Any, Final, cast, final import voluptuous as vol @@ -51,6 +51,7 @@ _LOGGER: Final = logging.getLogger(__name__) +ATTR_LAST_RESET: Final = "last_reset" # Deprecated, to be removed in 2021.11 ATTR_STATE_CLASS: Final = "state_class" DOMAIN: Final = "sensor" @@ -128,6 +129,7 @@ class SensorEntityDescription(EntityDescription): """A class that describes sensor entities.""" state_class: str | None = None + last_reset: datetime | None = None # Deprecated, to be removed in 2021.11 native_unit_of_measurement: str | None = None @@ -135,6 +137,7 @@ class SensorEntity(Entity): """Base class for sensor entities.""" entity_description: SensorEntityDescription + _attr_last_reset: datetime | None # Deprecated, to be removed in 2021.11 _attr_native_unit_of_measurement: str | None _attr_native_value: StateType = None _attr_state_class: str | None @@ -150,6 +153,15 @@ def state_class(self) -> str | None: return self.entity_description.state_class return None + @property + def last_reset(self) -> datetime | None: # Deprecated, to be removed in 2021.11 + """Return the time when the sensor was last reset, if any.""" + if hasattr(self, "_attr_last_reset"): + return self._attr_last_reset + if hasattr(self, "entity_description"): + return self.entity_description.last_reset + return None + @property def capability_attributes(self) -> Mapping[str, Any] | None: """Return the capability attributes.""" @@ -158,6 +170,15 @@ def capability_attributes(self) -> Mapping[str, Any] | None: return None + @final + @property + def state_attributes(self) -> dict[str, Any] | None: + """Return state attributes.""" + if last_reset := self.last_reset: + return {ATTR_LAST_RESET: last_reset.isoformat()} + + return None + @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 2a1d82305f0ff..83bdb2b1101f4 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -46,7 +46,7 @@ import homeassistant.util.temperature as temperature_util import homeassistant.util.volume as volume_util -from . import DOMAIN +from . import ATTR_LAST_RESET, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -54,11 +54,14 @@ STATE_CLASS_MEASUREMENT: { DEVICE_CLASS_BATTERY: {"mean", "min", "max"}, DEVICE_CLASS_HUMIDITY: {"mean", "min", "max"}, - DEVICE_CLASS_MONETARY: {"sum"}, DEVICE_CLASS_POWER: {"mean", "min", "max"}, DEVICE_CLASS_PRESSURE: {"mean", "min", "max"}, DEVICE_CLASS_TEMPERATURE: {"mean", "min", "max"}, PERCENTAGE: {"mean", "min", "max"}, + # Deprecated, support will be removed in Home Assistant 2021.11 + DEVICE_CLASS_ENERGY: {"sum"}, + DEVICE_CLASS_GAS: {"sum"}, + DEVICE_CLASS_MONETARY: {"sum"}, }, STATE_CLASS_TOTAL_INCREASING: { DEVICE_CLASS_ENERGY: {"sum"}, @@ -280,6 +283,13 @@ def compile_statistics( for fstate, state in fstates: + # Deprecated, will be removed in Home Assistant 2021.10 + if ( + "last_reset" not in state.attributes + and state_class == STATE_CLASS_MEASUREMENT + ): + continue + reset = False if old_state is None: reset = True @@ -333,6 +343,13 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) - state = hass.states.get(entity_id) assert state + if ( + "sum" in provided_statistics + and ATTR_LAST_RESET not in state.attributes + and state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + ): + continue + native_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if key not in UNIT_CONVERSIONS: diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 182ed0a058214..3a2572f8141c5 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -95,7 +95,6 @@ def test_compile_hourly_statistics( "mean": approx(mean), "min": approx(min), "max": approx(max), - "last_reset": None, "state": None, "sum": None, } @@ -145,7 +144,6 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes "mean": approx(16.440677966101696), "min": approx(10.0), "max": approx(30.0), - "last_reset": None, "state": None, "sum": None, } @@ -208,7 +206,6 @@ def test_compile_hourly_sum_statistics_amount( "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(factor * seq[2]), "sum": approx(factor * 10.0), }, @@ -218,89 +215,6 @@ def test_compile_hourly_sum_statistics_amount( "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), - "state": approx(factor * seq[5]), - "sum": approx(factor * 10.0), - }, - { - "statistic_id": "sensor.test1", - "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), - "max": None, - "mean": None, - "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), - "state": approx(factor * seq[8]), - "sum": approx(factor * 40.0), - }, - ] - } - assert "Error while processing event StatisticsTask" not in caplog.text - - -@pytest.mark.parametrize( - "device_class,unit,native_unit,factor", - [ - ("energy", "kWh", "kWh", 1), - ("energy", "Wh", "kWh", 1 / 1000), - ("monetary", "EUR", "EUR", 1), - ("monetary", "SEK", "SEK", 1), - ("gas", "m³", "m³", 1), - ("gas", "ft³", "m³", 0.0283168466), - ], -) -def test_compile_hourly_sum_statistics_no_reset( - hass_recorder, caplog, device_class, unit, native_unit, factor -): - """Test compiling hourly statistics.""" - zero = dt_util.utcnow() - hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] - setup_component(hass, "sensor", {}) - attributes = { - "device_class": device_class, - "state_class": "measurement", - "unit_of_measurement": unit, - } - seq = [10, 15, 20, 10, 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, - "last_reset": 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, - "last_reset": None, "state": approx(factor * seq[5]), "sum": approx(factor * 30.0), }, @@ -310,7 +224,6 @@ def test_compile_hourly_sum_statistics_no_reset( "max": None, "mean": None, "min": None, - "last_reset": None, "state": approx(factor * seq[8]), "sum": approx(factor * 60.0), }, @@ -370,7 +283,6 @@ def test_compile_hourly_sum_statistics_total_increasing( "max": None, "mean": None, "min": None, - "last_reset": None, "state": approx(factor * seq[2]), "sum": approx(factor * 10.0), }, @@ -380,7 +292,6 @@ def test_compile_hourly_sum_statistics_total_increasing( "max": None, "mean": None, "min": None, - "last_reset": None, "state": approx(factor * seq[5]), "sum": approx(factor * 50.0), }, @@ -390,7 +301,6 @@ def test_compile_hourly_sum_statistics_total_increasing( "max": None, "mean": None, "min": None, - "last_reset": None, "state": approx(factor * seq[8]), "sum": approx(factor * 80.0), }, @@ -415,8 +325,8 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): sns3_attr = {} sns4_attr = { "device_class": "energy", + "state_class": "measurement", "unit_of_measurement": "kWh", - "last_reset": None, } seq1 = [10, 15, 20, 10, 30, 40, 50, 60, 70] seq2 = [110, 120, 130, 0, 30, 45, 55, 65, 75] @@ -457,7 +367,6 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(20.0), "sum": approx(10.0), }, @@ -467,9 +376,8 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(40.0), - "sum": approx(10.0), + "sum": approx(30.0), }, { "statistic_id": "sensor.test1", @@ -477,9 +385,8 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(70.0), - "sum": approx(40.0), + "sum": approx(60.0), }, ] } @@ -530,7 +437,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): {"statistic_id": "sensor.test1", "unit_of_measurement": "kWh"}, {"statistic_id": "sensor.test2", "unit_of_measurement": "kWh"}, {"statistic_id": "sensor.test3", "unit_of_measurement": "kWh"}, - {"statistic_id": "sensor.test4", "unit_of_measurement": "kWh"}, ] stats = statistics_during_period(hass, zero) assert stats == { @@ -541,7 +447,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(20.0), "sum": approx(10.0), }, @@ -551,9 +456,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(40.0), - "sum": approx(10.0), + "sum": approx(30.0), }, { "statistic_id": "sensor.test1", @@ -561,9 +465,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(70.0), - "sum": approx(40.0), + "sum": approx(60.0), }, ], "sensor.test2": [ @@ -573,7 +476,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(130.0), "sum": approx(20.0), }, @@ -583,9 +485,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(45.0), - "sum": approx(-95.0), + "sum": approx(-65.0), }, { "statistic_id": "sensor.test2", @@ -593,9 +494,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(75.0), - "sum": approx(-65.0), + "sum": approx(-35.0), }, ], "sensor.test3": [ @@ -605,7 +505,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(zero), "state": approx(5.0 / 1000), "sum": approx(5.0 / 1000), }, @@ -615,9 +514,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(50.0 / 1000), - "sum": approx(30.0 / 1000), + "sum": approx(50.0 / 1000), }, { "statistic_id": "sensor.test3", @@ -625,41 +523,8 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): "max": None, "mean": None, "min": None, - "last_reset": process_timestamp_to_utc_isoformat(four), "state": approx(90.0 / 1000), - "sum": approx(70.0 / 1000), - }, - ], - "sensor.test4": [ - { - "statistic_id": "sensor.test4", - "start": process_timestamp_to_utc_isoformat(zero), - "max": None, - "mean": None, - "min": None, - "last_reset": None, - "state": approx(5.0), - "sum": approx(5.0), - }, - { - "statistic_id": "sensor.test4", - "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), - "max": None, - "mean": None, - "min": None, - "last_reset": None, - "state": approx(50.0), - "sum": approx(50.0), - }, - { - "statistic_id": "sensor.test4", - "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), - "max": None, - "mean": None, - "min": None, - "last_reset": None, - "state": approx(90.0), - "sum": approx(90.0), + "sum": approx(90.0 / 1000), }, ], } @@ -710,7 +575,6 @@ def test_compile_hourly_statistics_unchanged( "mean": approx(value), "min": approx(value), "max": approx(value), - "last_reset": None, "state": None, "sum": None, } @@ -742,7 +606,6 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): "mean": approx(21.1864406779661), "min": approx(10.0), "max": approx(25.0), - "last_reset": None, "state": None, "sum": None, } @@ -799,7 +662,6 @@ def test_compile_hourly_statistics_unavailable( "mean": approx(value), "min": approx(value), "max": approx(value), - "last_reset": None, "state": None, "sum": None, } From 2cd1a2821bd6db63ad1f9ffa1de3155b68e10ecf Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 17 Aug 2021 21:17:44 +0200 Subject: [PATCH 08/14] Re-add and update deprecation warning --- homeassistant/components/sensor/__init__.py | 20 +++++++++++++++++++- tests/components/sensor/test_init.py | 21 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 18de56e4a5163..9957736b5cc9a 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -175,7 +175,25 @@ def capability_attributes(self) -> Mapping[str, Any] | None: def state_attributes(self) -> dict[str, Any] | None: """Return state attributes.""" if last_reset := self.last_reset: - return {ATTR_LAST_RESET: last_reset.isoformat()} + if ( + last_reset is not None + and self.state_class == STATE_CLASS_MEASUREMENT + and not self._last_reset_reported + ): + self._last_reset_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Entity %s (%s) with state_class %s has set last_reset. Setting " + "last_reset is deprecated and will be unsupported from Home " + "Assistant Core 2021.11. Please update your configuration if " + "state_class is manually configured, otherwise %s", + self.entity_id, + type(self), + self.state_class, + report_issue, + ) + + return {ATTR_LAST_RESET: last_reset.isoformat()} return None diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index f09cd4894898b..5ff2cad9edcbd 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1,6 +1,7 @@ """The test for sensor device automation.""" from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util async def test_deprecated_temperature_conversion( @@ -28,3 +29,23 @@ async def test_deprecated_temperature_conversion( "your configuration if device_class is manually configured, otherwise report it " "to the custom component author." ) in caplog.text + + +async def test_deprecated_last_reset(hass, caplog, enable_custom_integrations): + """Test warning on deprecated last reset.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", state_class="measurement", last_reset=dt_util.utc_from_timestamp(0) + ) + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + assert ( + "Entity sensor.test () " + "with state_class measurement has set last_reset. Setting last_reset is " + "deprecated and will be unsupported from Home Assistant Core 2021.11. Please " + "update your configuration if state_class is manually configured, otherwise " + "report it to the custom component author." + ) in caplog.text From ef014271a07eaf8f4a32dc5de2460994bb78affe Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 17 Aug 2021 21:20:45 +0200 Subject: [PATCH 09/14] Update tests --- tests/components/recorder/test_statistics.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 0468cc26a2355..83995b0c0acf7 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -44,7 +44,6 @@ def test_compile_hourly_statistics(hass_recorder): "mean": approx(14.915254237288135), "min": approx(10.0), "max": approx(20.0), - "last_reset": None, "state": None, "sum": None, } @@ -54,7 +53,6 @@ def test_compile_hourly_statistics(hass_recorder): "mean": approx(20.0), "min": approx(20.0), "max": approx(20.0), - "last_reset": None, "state": None, "sum": None, } @@ -127,7 +125,6 @@ def test_rename_entity(hass_recorder): "mean": approx(14.915254237288135), "min": approx(10.0), "max": approx(20.0), - "last_reset": None, "state": None, "sum": None, } From e3b60072f8a93572f311ee25b92d1151be204b1e Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 17 Aug 2021 22:28:17 +0200 Subject: [PATCH 10/14] Fix utility_meter --- homeassistant/components/utility_meter/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index ed4af4228ba68..84533efdcf59d 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -349,7 +349,7 @@ def extra_state_attributes(self): ATTR_SOURCE_ID: self._sensor_source_id, ATTR_STATUS: PAUSED if self._collecting is None else COLLECTING, ATTR_LAST_PERIOD: self._last_period, - ATTR_LAST_RESET: self._last_reset, + ATTR_LAST_RESET: self._last_reset.isoformat(), } if self._period is not None: state_attr[ATTR_PERIOD] = self._period From bbf798d0dfeaa58ed35f5ab8d1fda0e777a2743f Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 17 Aug 2021 23:12:20 +0200 Subject: [PATCH 11/14] Update EnergyCostSensor --- homeassistant/components/energy/sensor.py | 25 +++++++------- tests/components/energy/test_sensor.py | 41 +++++++++-------------- 2 files changed, 28 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index cd017f098f870..497c762add98c 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -16,10 +16,10 @@ ENERGY_WATT_HOUR, VOLUME_CUBIC_METERS, ) -from homeassistant.core import HomeAssistant, State, callback, split_entity_id +from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from .const import DOMAIN from .data import EnergyManager, async_get_manager @@ -203,10 +203,10 @@ def __init__( self._attr_device_class = DEVICE_CLASS_MONETARY self._attr_state_class = STATE_CLASS_TOTAL_INCREASING self._config = config - self._last_energy_sensor_state: State | None = None + self._last_energy_sensor_state: StateType | None = None self._cur_value = 0.0 - def _reset(self, energy_state: State) -> None: + def _reset(self, energy_state: StateType) -> None: """Reset the cost sensor.""" self._attr_native_value = 0.0 self._cur_value = 0.0 @@ -256,7 +256,7 @@ def _update_cost(self) -> None: if self._last_energy_sensor_state is None: # Initialize as it's the first time all required entities are in place. - self._reset(energy_state) + self._reset(energy_state.state) return energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -277,16 +277,15 @@ def _update_cost(self) -> None: ) return - if energy < float(self._last_energy_sensor_state.state): + if energy < float(self._last_energy_sensor_state): # Energy meter was reset, reset cost sensor too - self._reset(energy_state) - else: - # Update with newly incurred cost - old_energy_value = float(self._last_energy_sensor_state.state) - self._cur_value += (energy - old_energy_value) * energy_price - self._attr_native_value = round(self._cur_value, 2) + self._reset(0) + # Update with newly incurred cost + old_energy_value = float(self._last_energy_sensor_state) + self._cur_value += (energy - old_energy_value) * energy_price + self._attr_native_value = round(self._cur_value, 2) - self._last_energy_sensor_state = energy_state + self._last_energy_sensor_state = energy_state.state async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 1e89c05fbd6f3..ea183ec52f496 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -7,9 +7,8 @@ from homeassistant.components.energy import data from homeassistant.components.sensor import ( - ATTR_LAST_RESET, ATTR_STATE_CLASS, - STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, ) from homeassistant.components.sensor.recorder import compile_statistics from homeassistant.const import ( @@ -131,14 +130,13 @@ def _compile_statistics(_): } now = dt_util.utcnow() - last_reset = dt_util.utc_from_timestamp(0).isoformat() # Optionally initialize dependent entities if initial_energy is not None: hass.states.async_set( usage_sensor_entity_id, initial_energy, - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, ) hass.states.async_set("sensor.energy_price", "1") @@ -148,9 +146,7 @@ def _compile_statistics(_): state = hass.states.get(cost_sensor_entity_id) assert state.state == initial_cost assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY - if initial_cost != "unknown": - assert state.attributes[ATTR_LAST_RESET] == now.isoformat() - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # Optional late setup of dependent entities @@ -160,7 +156,6 @@ def _compile_statistics(_): usage_sensor_entity_id, "0", { - "last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, ) @@ -169,8 +164,7 @@ def _compile_statistics(_): state = hass.states.get(cost_sensor_entity_id) assert state.state == "0.0" assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_MONETARY - assert state.attributes[ATTR_LAST_RESET] == now.isoformat() - assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_MEASUREMENT + assert state.attributes[ATTR_STATE_CLASS] == STATE_CLASS_TOTAL_INCREASING assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "EUR" # # Unique ID temp disabled @@ -182,7 +176,7 @@ def _compile_statistics(_): hass.states.async_set( usage_sensor_entity_id, "10", - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) @@ -206,7 +200,7 @@ def _compile_statistics(_): hass.states.async_set( usage_sensor_entity_id, "14.5", - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, ) await hass.async_block_till_done() state = hass.states.get(cost_sensor_entity_id) @@ -218,32 +212,31 @@ def _compile_statistics(_): assert cost_sensor_entity_id in statistics assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 19.0 - # Energy sensor is reset, with start point at 4kWh - last_reset = (now + timedelta(seconds=1)).isoformat() + # Energy sensor is reset, with initial state at 4kWh, 0 kWh is used as zero-point hass.states.async_set( usage_sensor_entity_id, "4", - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {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 == "0.0" # 0 EUR + (4-4) kWh * 2 EUR/kWh = 0 EUR + assert state.state == "8.0" # 0 EUR + (4-0) kWh * 2 EUR/kWh = 8 EUR # Energy use bumped to 10 kWh hass.states.async_set( usage_sensor_entity_id, "10", - {"last_reset": last_reset, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {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 == "12.0" # 0 EUR + (10-4) kWh * 2 EUR/kWh = 12 EUR + assert state.state == "20.0" # 8 EUR + (10-4) kWh * 2 EUR/kWh = 20 EUR # Check generated statistics await async_wait_recording_done_without_instance(hass) statistics = await hass.loop.run_in_executor(None, _compile_statistics, hass) assert cost_sensor_entity_id in statistics - assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 31.0 + assert statistics[cost_sensor_entity_id]["stat"]["sum"] == 39.0 async def test_cost_sensor_handle_wh(hass, hass_storage) -> None: @@ -272,12 +265,11 @@ async def test_cost_sensor_handle_wh(hass, hass_storage) -> None: } now = dt_util.utcnow() - last_reset = dt_util.utc_from_timestamp(0).isoformat() hass.states.async_set( "sensor.energy_consumption", 10000, - {"last_reset": last_reset, "unit_of_measurement": ENERGY_WATT_HOUR}, + {"unit_of_measurement": ENERGY_WATT_HOUR}, ) with patch("homeassistant.util.dt.utcnow", return_value=now): @@ -290,7 +282,7 @@ async def test_cost_sensor_handle_wh(hass, hass_storage) -> None: hass.states.async_set( "sensor.energy_consumption", 20000, - {"last_reset": last_reset, "unit_of_measurement": ENERGY_WATT_HOUR}, + {"unit_of_measurement": ENERGY_WATT_HOUR}, ) await hass.async_block_till_done() @@ -318,12 +310,11 @@ async def test_cost_sensor_handle_gas(hass, hass_storage) -> None: } now = dt_util.utcnow() - last_reset = dt_util.utc_from_timestamp(0).isoformat() hass.states.async_set( "sensor.gas_consumption", 100, - {"last_reset": last_reset, "unit_of_measurement": VOLUME_CUBIC_METERS}, + {"unit_of_measurement": VOLUME_CUBIC_METERS}, ) with patch("homeassistant.util.dt.utcnow", return_value=now): @@ -336,7 +327,7 @@ async def test_cost_sensor_handle_gas(hass, hass_storage) -> None: hass.states.async_set( "sensor.gas_consumption", 200, - {"last_reset": last_reset, "unit_of_measurement": VOLUME_CUBIC_METERS}, + {"unit_of_measurement": VOLUME_CUBIC_METERS}, ) await hass.async_block_till_done() From e26b4eca1d2d08a39f78afe41fc2bebb5e0a489a Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 17 Aug 2021 23:36:12 +0200 Subject: [PATCH 12/14] Tweak --- homeassistant/components/sensor/recorder.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 83bdb2b1101f4..2cbca09c09d92 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -304,10 +304,9 @@ def compile_statistics( _sum += new_state - old_state # ..and update the starting point new_state = fstate - old_last_reset = last_reset - # Force a new cycle for STATE_CLASS_TOTAL_INCREASING to start at 0 - if state_class == STATE_CLASS_TOTAL_INCREASING and old_state: - old_state = 0 + # Force a new cycle to start at 0 + if old_state is not None: + old_state = 0.0 else: old_state = new_state else: From 11ab0e2f7885b55b87b79a39692984d32aa5931a Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 18 Aug 2021 08:16:33 +0200 Subject: [PATCH 13/14] Fix rebase mistake --- homeassistant/components/sensor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 9957736b5cc9a..94fb08c66b1e9 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -193,7 +193,7 @@ def state_attributes(self) -> dict[str, Any] | None: report_issue, ) - return {ATTR_LAST_RESET: last_reset.isoformat()} + return {ATTR_LAST_RESET: last_reset.isoformat()} return None From dadba905a638f711d75541e6d2003839ec7bbea8 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 18 Aug 2021 08:21:25 +0200 Subject: [PATCH 14/14] Fix test --- tests/components/history/test_init.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 7909d8f02396d..8de448436265f 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -911,7 +911,6 @@ async def test_statistics_during_period( "mean": approx(value), "min": approx(value), "max": approx(value), - "last_reset": None, "state": None, "sum": None, }