diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 0518495a0f008..370b36bf1fcdb 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -96,11 +96,14 @@ # 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 @@ def state_attributes(self) -> dict[str, Any] | None: 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 0054b01abd2c8..5bb6b6c16f8cc 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -15,6 +15,7 @@ DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, STATE_CLASSES, ) @@ -56,10 +57,12 @@ 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"}, } @@ -393,7 +396,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 542ea3296ce50..f91ddd9220665 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -8,6 +8,7 @@ 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 7463cc6755aa5..7859d133c2975 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 115473c23de74..59d336f3469d9 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", [