diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 0518495a0f0..370b36bf1fc 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -96,11 +96,14 @@ DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) # The state represents a measurement in present time STATE_CLASS_MEASUREMENT: Final = "measurement" +# The state represents a total amount, e.g. net energy consumption +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, ] @@ -214,9 +217,10 @@ class SensorEntity(Entity): 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", + "last_reset for entities with state_class other than 'total' is " + "deprecated and will be removed 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, diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 8bf251ffb18..c6c8482669e 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -15,6 +15,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, STATE_CLASSES, ) @@ -56,10 +57,12 @@ DEVICE_CLASS_STATISTICS: dict[str, dict[str, set[str]]] = { DEVICE_CLASS_GAS: {"sum"}, DEVICE_CLASS_MONETARY: {"sum"}, }, + STATE_CLASS_TOTAL: {}, STATE_CLASS_TOTAL_INCREASING: {}, } DEFAULT_STATISTICS = { STATE_CLASS_MEASUREMENT: {"mean", "min", "max"}, + STATE_CLASS_TOTAL: {"sum"}, STATE_CLASS_TOTAL_INCREASING: {"sum"}, } @@ -389,7 +392,7 @@ def compile_statistics( # noqa: C901 for fstate, state in fstates: - # Deprecated, will be removed in Home Assistant 2021.10 + # Deprecated, will be removed in Home Assistant 2021.11 if ( "last_reset" not in state.attributes and state_class == STATE_CLASS_MEASUREMENT diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 542ea3296ce..f91ddd92206 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -8,6 +8,7 @@ import pytest from homeassistant.components.energy import data from homeassistant.components.sensor import ( ATTR_STATE_CLASS, + STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, ) from homeassistant.components.sensor.recorder import compile_statistics @@ -357,7 +358,7 @@ async def test_cost_sensor_handle_gas(hass, hass_storage) -> None: assert state.state == "50.0" -@pytest.mark.parametrize("state_class", [None]) +@pytest.mark.parametrize("state_class", [None, STATE_CLASS_TOTAL]) async def test_cost_sensor_wrong_state_class( hass, hass_storage, caplog, state_class ) -> None: diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 7463cc6755a..7859d133c29 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -45,10 +45,11 @@ async def test_deprecated_last_reset(hass, caplog, enable_custom_integrations): 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." + "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.11. 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 aeeab317eb1..41fa80d3f24 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -191,7 +191,7 @@ 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"]) +@pytest.mark.parametrize("state_class", ["measurement", "total"]) @pytest.mark.parametrize( "device_class,unit,native_unit,factor", [ @@ -349,6 +349,88 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( 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_total_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": "total", + "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), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(factor * seq[8]), + "sum": approx(factor * 60.0), + }, + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text + + @pytest.mark.parametrize( "device_class,unit,native_unit,factor", [