Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions homeassistant/components/sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,16 @@

# The state represents a measurement in present time
STATE_CLASS_MEASUREMENT: Final = "measurement"

STATE_CLASSES: Final[list[str]] = [STATE_CLASS_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,
]

STATE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.In(STATE_CLASSES))

Expand Down Expand Up @@ -118,6 +126,7 @@ class SensorEntity(Entity):
_attr_native_unit_of_measurement: str | None
_attr_native_value: StateType = None
_attr_state_class: str | None
_last_reset_reported = False
_temperature_conversion_reported = False

@property
Expand Down Expand Up @@ -151,6 +160,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:
if (
last_reset is not None
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed, L162 will not continue if this is None

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, added to #54624

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
Expand Down
93 changes: 69 additions & 24 deletions homeassistant/components/sensor/recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL,
STATE_CLASS_TOTAL_INCREASING,
STATE_CLASSES,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
Expand Down Expand Up @@ -50,15 +53,27 @@
_LOGGER = logging.getLogger(__name__)

DEVICE_CLASS_OR_UNIT_STATISTICS = {
DEVICE_CLASS_BATTERY: {"mean", "min", "max"},
DEVICE_CLASS_ENERGY: {"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"},
DEVICE_CLASS_GAS: {"sum"},
PERCENTAGE: {"mean", "min", "max"},
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_HUMIDITY: {"mean", "min", "max"},
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"},
DEVICE_CLASS_GAS: {"sum"},
},
}

# Normalized units which will be stored in the statistics table
Expand Down Expand Up @@ -109,24 +124,28 @@
WARN_UNSUPPORTED_UNIT = set()


def _get_entities(hass: HomeAssistant) -> list[tuple[str, str]]:
"""Get (entity_id, device_class) of all sensors for which to compile statistics."""
def _get_entities(hass: HomeAssistant) -> list[tuple[str, str, str]]:
"""Get (entity_id, state_class, key) of all sensors for which to compile statistics.

Key is either a device class or a unit and is used to index the
DEVICE_CLASS_OR_UNIT_STATISTICS map.
"""
all_sensors = hass.states.all(DOMAIN)
entity_ids = []

for state in all_sensors:
if state.attributes.get(ATTR_STATE_CLASS) != STATE_CLASS_MEASUREMENT:
if (state_class := state.attributes.get(ATTR_STATE_CLASS)) not in STATE_CLASSES:
continue

if (
key := state.attributes.get(ATTR_DEVICE_CLASS)
) in DEVICE_CLASS_OR_UNIT_STATISTICS:
entity_ids.append((state.entity_id, key))
) in DEVICE_CLASS_OR_UNIT_STATISTICS[state_class]:
entity_ids.append((state.entity_id, state_class, key))

if (
key := state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
) in DEVICE_CLASS_OR_UNIT_STATISTICS:
entity_ids.append((state.entity_id, key))
) in DEVICE_CLASS_OR_UNIT_STATISTICS[state_class]:
entity_ids.append((state.entity_id, state_class, key))

return entity_ids

Expand Down Expand Up @@ -228,8 +247,8 @@ def compile_statistics(
hass, start - datetime.timedelta.resolution, end, [i[0] for i in entities]
)

for entity_id, key in entities:
wanted_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[key]
for entity_id, state_class, key in entities:
wanted_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[state_class][key]

if entity_id not in history_list:
continue
Expand Down Expand Up @@ -272,9 +291,28 @@ def compile_statistics(

for fstate, state in fstates:

if "last_reset" not in state.attributes:
# Deprecated, will be removed in Home Assistant 2021.10
if (
"last_reset" not in state.attributes
and state_class == STATE_CLASS_MEASUREMENT
):
continue
if (last_reset := state.attributes["last_reset"]) != old_last_reset:

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:
reset = True
elif state_class == STATE_CLASS_TOTAL_INCREASING and (
old_state is None or fstate < old_state
Copy link
Copy Markdown
Member

@TomBrien TomBrien Aug 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth usings omething like fstate < 0.9 * old_state? I'm just thinking of cases where a slightly noisy energy sensor is being used in an integration to calculate power. If the sensor is measuring 0 +/- some noise it is possible for the value to creep down a small amount without being a true reset

Copy link
Copy Markdown
Contributor Author

@emontnemery emontnemery Aug 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that needs to be handled by the integration if it claims STATE_CLASS_TOTAL_INCREASING.
In the example with integrating a noisy power sensor (I think that's what you meant?) it could perhaps be handled by ignoring negative power?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I think it's best that the integration claiming that the sensor will only increase ensures that is true. I thought of some others where a less than could be an issue but in all cases the integration should know what it's doing better

):
reset = True

if reset:
# The sensor has been reset, update the sum
if old_state is not None:
_sum += new_state - old_state
Expand All @@ -285,14 +323,21 @@ def compile_statistics(
else:
new_state = fstate

if last_reset is None or new_state is None or old_state is None:
# 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)
continue

# Update the sum with the last state
_sum += new_state - old_state
stat["last_reset"] = dt_util.parse_datetime(last_reset)
if last_reset is not None:
stat["last_reset"] = dt_util.parse_datetime(last_reset)
stat["sum"] = _sum
stat["state"] = new_state

Expand All @@ -307,8 +352,8 @@ def list_statistic_ids(hass: HomeAssistant, statistic_type: str | None = None) -

statistic_ids = {}

for entity_id, key in entities:
provided_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[key]
for entity_id, state_class, key in entities:
provided_statistics = DEVICE_CLASS_OR_UNIT_STATISTICS[state_class][key]

if statistic_type is not None and statistic_type not in provided_statistics:
continue
Expand Down
22 changes: 22 additions & 0 deletions tests/components/sensor/test_init.py
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -28,3 +29,24 @@ 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 (<class 'custom_components.test.sensor.MockSensor'>) "
"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
Loading