diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 3f4a1e861b3..aa0aa6c5f0b 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, SensorEntity, ) from homeassistant.const import CONF_HOST, PERCENTAGE, TEMP_CELSIUS @@ -20,11 +21,16 @@ from .helpers import import_device _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - "temperature": ("Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE), - "air_quality": ("Air Quality", None, None), - "humidity": ("Humidity", PERCENTAGE, DEVICE_CLASS_HUMIDITY), - "light": ("Light", None, DEVICE_CLASS_ILLUMINANCE), - "noise": ("Noise", None, None), + "temperature": ( + "Temperature", + TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + ), + "air_quality": ("Air Quality", None, None, None), + "humidity": ("Humidity", PERCENTAGE, DEVICE_CLASS_HUMIDITY, None), + "light": ("Light", None, DEVICE_CLASS_ILLUMINANCE, None), + "noise": ("Noise", None, None, None), } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -101,6 +107,11 @@ class BroadlinkSensor(SensorEntity): """Return device class.""" return SENSOR_TYPES[self._monitored_condition][2] + @property + def state_class(self): + """Return state class.""" + return SENSOR_TYPES[self._monitored_condition][3] + @property def device_info(self): """Return device info.""" diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index ba3be37da42..96963302139 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -14,7 +14,11 @@ from pydeconz.sensor import ( Thermostat, ) -from homeassistant.components.sensor import DOMAIN, SensorEntity +from homeassistant.components.sensor import ( + DOMAIN, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_VOLTAGE, @@ -60,6 +64,10 @@ ICON = { Temperature: "mdi:thermometer", } +STATE_CLASS = { + Temperature: STATE_CLASS_MEASUREMENT, +} + UNIT_OF_MEASUREMENT = { Consumption: ENERGY_KILO_WATT_HOUR, Humidity: PERCENTAGE, @@ -161,6 +169,11 @@ class DeconzSensor(DeconzDevice, SensorEntity): """Return the icon to use in the frontend.""" return ICON.get(type(self._device)) + @property + def state_class(self): + """Return the state class of the sensor.""" + return STATE_CLASS.get(type(self._device)) + @property def unit_of_measurement(self): """Return the unit of measurement of this sensor.""" @@ -233,6 +246,11 @@ class DeconzTemperature(DeconzDevice, SensorEntity): """Return the class of the sensor.""" return DEVICE_CLASS_TEMPERATURE + @property + def state_class(self): + """Return the state class of the sensor.""" + return STATE_CLASS_MEASUREMENT + @property def unit_of_measurement(self): """Return the unit of measurement of this sensor.""" diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 988dfc31b35..75d31fb61ce 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -6,7 +6,7 @@ from aiohue.sensors import ( TYPE_ZLL_TEMPERATURE, ) -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, @@ -87,6 +87,11 @@ class HueTemperature(GenericHueGaugeSensorEntity): return self.sensor.temperature / 100 + @property + def state_class(self): + """Return the state class of the sensor.""" + return STATE_CLASS_MEASUREMENT + class HueBattery(GenericHueSensor, SensorEntity): """Battery class for when a batt-powered device is only represented as an event.""" diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 9445c792c7b..bcaa6385100 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -151,6 +151,7 @@ class BlockAttributeDescription: unit: None | str | Callable[[dict], str] = None value: Callable[[Any], Any] = lambda val: val device_class: str | None = None + state_class: str | None = None default_enabled: bool = True available: Callable[[aioshelly.Block], bool] | None = None # Callable (settings, block), return true if entity should be removed diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 9337011ba16..006ee166423 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -135,6 +135,7 @@ SENSORS = { unit=temperature_unit, value=lambda value: round(value, 1), device_class=sensor.DEVICE_CLASS_TEMPERATURE, + state_class=sensor.STATE_CLASS_MEASUREMENT, available=lambda block: block.extTemp != 999, ), ("sensor", "humidity"): BlockAttributeDescription( @@ -231,6 +232,11 @@ class ShellyRestSensor(ShellyRestAttributeEntity, SensorEntity): """Return value of sensor.""" return self.attribute_value + @property + def state_class(self): + """State class of sensor.""" + return self.description.state_class + @property def unit_of_measurement(self): """Return unit of sensor.""" diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index 02a04467194..5faea128594 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from hatasmota import const as hc, status_sensor from homeassistant.components import sensor -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import STATE_CLASS_MEASUREMENT, SensorEntity from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, @@ -46,6 +46,7 @@ from .discovery import TASMOTA_DISCOVERY_ENTITY_NEW from .mixins import TasmotaAvailability, TasmotaDiscoveryUpdate DEVICE_CLASS = "device_class" +STATE_CLASS = "state_class" ICON = "icon" # A Tasmota sensor type may be mapped to either a device class or an icon, not both @@ -89,7 +90,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP = { hc.SENSOR_STATUS_SIGNAL: {DEVICE_CLASS: DEVICE_CLASS_SIGNAL_STRENGTH}, hc.SENSOR_STATUS_RSSI: {ICON: "mdi:access-point"}, hc.SENSOR_STATUS_SSID: {ICON: "mdi:access-point-network"}, - hc.SENSOR_TEMPERATURE: {DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE}, + hc.SENSOR_TEMPERATURE: { + DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, hc.SENSOR_TODAY: {DEVICE_CLASS: DEVICE_CLASS_POWER}, hc.SENSOR_TOTAL: {DEVICE_CLASS: DEVICE_CLASS_POWER}, hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, @@ -172,6 +176,14 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity): ) return class_or_icon.get(DEVICE_CLASS) + @property + def state_class(self) -> str | None: + """Return the state class of the sensor.""" + class_or_icon = SENSOR_DEVICE_CLASS_ICON_MAP.get( + self._tasmota_entity.quantity, {} + ) + return class_or_icon.get(STATE_CLASS) + @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index ac9a7ab4543..ed551a6dd49 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -12,7 +12,11 @@ from miio.gateway.gateway import ( ) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -66,11 +70,15 @@ class SensorType: unit: str = None icon: str = None device_class: str = None + state_class: str = None GATEWAY_SENSOR_TYPES = { "temperature": SensorType( - unit=TEMP_CELSIUS, icon=None, device_class=DEVICE_CLASS_TEMPERATURE + unit=TEMP_CELSIUS, + icon=None, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), "humidity": SensorType( unit=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_HUMIDITY @@ -245,6 +253,11 @@ class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity): """Return the device class of this entity.""" return GATEWAY_SENSOR_TYPES[self._data_key].device_class + @property + def state_class(self): + """Return the state class of this entity.""" + return GATEWAY_SENSOR_TYPES[self._data_key].state_class + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 616e0345828..a6d7ae4ce9e 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -15,6 +15,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DOMAIN, + STATE_CLASS_MEASUREMENT, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -101,6 +102,7 @@ class Sensor(ZhaEntity, SensorEntity): _device_class: str | None = None _divisor: int = 1 _multiplier: int = 1 + _state_class: str | None = None _unit: str | None = None def __init__( @@ -126,6 +128,11 @@ class Sensor(ZhaEntity, SensorEntity): """Return device class from component DEVICE_CLASSES.""" return self._device_class + @property + def state_class(self) -> str | None: + """Return the state class of this entity, from STATE_CLASSES, if any.""" + return self._state_class + @property def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" @@ -285,6 +292,7 @@ class Temperature(Sensor): SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_TEMPERATURE _divisor = 100 + _state_class = STATE_CLASS_MEASUREMENT _unit = TEMP_CELSIUS diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index b23b11f3424..dcc21b236e8 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -87,6 +88,7 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): super().__init__(config_entry, client, info) self._name = self.generate_name(include_value_name=True) self._device_class = self._get_device_class() + self._state_class = self._get_state_class() def _get_device_class(self) -> str | None: """ @@ -113,11 +115,29 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): return DEVICE_CLASS_ILLUMINANCE return None + def _get_state_class(self) -> str | None: + """ + Get the state class of the sensor. + + This should be run once during initialization so we don't have to calculate + this value on every state update. + """ + if isinstance(self.info.primary_value.property_, str): + property_lower = self.info.primary_value.property_.lower() + if "temperature" in property_lower: + return STATE_CLASS_MEASUREMENT + return None + @property def device_class(self) -> str | None: """Return the device class of the sensor.""" return self._device_class + @property + def state_class(self) -> str | None: + """Return the state class of the sensor.""" + return self._state_class + @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index e5d31705a4f..5cc75c28a73 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -27,7 +27,7 @@ async def test_a1_sensor_setup(hass): assert mock_api.check_sensors_raw.call_count == 1 device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 5 sensors_and_states = { @@ -62,7 +62,7 @@ async def test_a1_sensor_update(hass): device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 5 mock_api.check_sensors_raw.return_value = { @@ -104,7 +104,7 @@ async def test_rm_pro_sensor_setup(hass): assert mock_api.check_sensors.call_count == 1 device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 1 sensors_and_states = { @@ -127,7 +127,7 @@ async def test_rm_pro_sensor_update(hass): device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 1 mock_api.check_sensors.return_value = {"temperature": 25.8} @@ -159,7 +159,7 @@ async def test_rm_pro_filter_crazy_temperature(hass): device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 1 mock_api.check_sensors.return_value = {"temperature": -7} @@ -189,7 +189,7 @@ async def test_rm_mini3_no_sensor(hass): assert mock_api.check_sensors.call_count <= 1 device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 0 @@ -207,7 +207,7 @@ async def test_rm4_pro_hts2_sensor_setup(hass): assert mock_api.check_sensors.call_count == 1 device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 2 sensors_and_states = { @@ -233,7 +233,7 @@ async def test_rm4_pro_hts2_sensor_update(hass): device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 2 mock_api.check_sensors.return_value = {"temperature": 16.8, "humidity": 34.0}