From e78b7dafa503d2eb4da549712bb2272cd29b2f67 Mon Sep 17 00:00:00 2001 From: Jason Cheatham Date: Fri, 29 Dec 2023 15:01:14 -0700 Subject: [PATCH] refactor: make better use of HASS sensor base class - Don't re-implement sensor property methods unless it's really necessary. Instead, implement the values that the existing property methods depend on. - Fix automatic temperature conversions not working --- custom_components/hubitat/device.py | 7 + .../hubitat/hubitatmaker/types.py | 9 +- custom_components/hubitat/sensor.py | 133 +++++++----------- 3 files changed, 62 insertions(+), 87 deletions(-) diff --git a/custom_components/hubitat/device.py b/custom_components/hubitat/device.py index 5b2d7d1..4aafa46 100644 --- a/custom_components/hubitat/device.py +++ b/custom_components/hubitat/device.py @@ -90,6 +90,13 @@ def get_attr(self, attr: str) -> float | int | str | None: return self._device.attributes[attr].value return None + @callback + def get_attr_unit(self, attr: str) -> str | None: + """Get the unit of an attribute.""" + if attr in self._device.attributes: + return self._device.attributes[attr].unit + return None + @callback def get_float_attr(self, attr: str) -> float | None: """Get the current value of an attribute.""" diff --git a/custom_components/hubitat/hubitatmaker/types.py b/custom_components/hubitat/hubitatmaker/types.py index 4bf5f9b..56d6f0c 100644 --- a/custom_components/hubitat/hubitatmaker/types.py +++ b/custom_components/hubitat/hubitatmaker/types.py @@ -25,12 +25,13 @@ def values(self) -> list[str] | None: return None return self._properties["values"] - def update_value(self, value: str | float) -> None: + def update_value(self, value: str | float, unit: str | None = None) -> None: self._properties["currentValue"] = value + self._properties["unit"] = unit @property - def unit(self) -> str: - return self._properties["unit"] + def unit(self) -> str | None: + return self._properties.get("unit") def __iter__(self): for key in "name", "type", "value", "unit": @@ -98,7 +99,7 @@ def update_attr( self, attr_name: str, value: str | int, value_unit: str | None ) -> None: attr = self.attributes[attr_name] - attr.update_value(value) + attr.update_value(value, value_unit) self._last_update = time() def update_state(self, properties: dict[str, Any]) -> None: diff --git a/custom_components/hubitat/sensor.py b/custom_components/hubitat/sensor.py index fe8678d..3ff87e0 100644 --- a/custom_components/hubitat/sensor.py +++ b/custom_components/hubitat/sensor.py @@ -4,8 +4,11 @@ from logging import getLogger from typing import Any, Type -from homeassistant.components.number import NumberDeviceClass -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, @@ -32,7 +35,6 @@ ATTR_HSM_STATUS, ATTR_MODE, TEMP_F, - DeviceType, ) from .device import HubitatEntity from .entities import create_and_add_entities @@ -44,14 +46,11 @@ _LOGGER = getLogger(__name__) -class HubitatSensor(HubitatEntity): +class HubitatSensor(HubitatEntity, SensorEntity): """A generic Hubitat sensor.""" _attribute: str _attribute_name: str | None = None - _units: str | None - _device_class: str | None = None - _state_class: str | None = None _enabled_default: bool | None = None def __init__( @@ -60,8 +59,8 @@ def __init__( attribute: str | None = None, attribute_name: str | None = None, units: str | None = None, - device_class: str | None = None, - state_class: str | None = None, + device_class: SensorDeviceClass | None = None, + state_class: SensorStateClass | None = None, enabled_default: bool | None = None, **kwargs: Any, ): @@ -73,11 +72,11 @@ def __init__( if attribute_name is not None: self._attribute_name = attribute_name if units is not None: - self._units = units + self._attr_native_unit_of_measurement = units if device_class is not None: - self._device_class = device_class + self._attr_device_class = device_class if state_class is not None: - self._state_class = state_class + self._attr_state_class = state_class if enabled_default is not None: self._enabled_default = enabled_default @@ -86,16 +85,6 @@ def device_attrs(self) -> tuple[str, ...] | None: """Return this entity's associated attributes""" return (self._attribute,) - @property - def device_class(self) -> str | None: - """Return this sensor's device class.""" - return self._device_class - - @property - def state_class(self) -> str | None: - """Return this sensor's state class.""" - return self._state_class - @property def name(self) -> str: """Return this sensor's display name.""" @@ -105,8 +94,8 @@ def name(self) -> str: return f"{super().name} {attr_name}".title() @property - def state(self) -> float | int | str | None: - """Return this sensor's current state.""" + def native_value(self) -> float | int | str | None: + """Return this sensor's current value.""" return self.get_attr(self._attribute) @property @@ -114,14 +103,6 @@ def unique_id(self) -> str: """Return a unique ID for this sensor.""" return f"{super().unique_id}::sensor::{self._attribute}" - @property - def unit_of_measurement(self) -> str | None: - """Return the units for this sensor's value.""" - try: - return self._units - except AttributeError: - return None - @property def entity_registry_enabled_default(self) -> bool: """Update sensors are disabled by default.""" @@ -137,7 +118,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a battery sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.BATTERY - self._units = PERCENTAGE + self._attr_native_unit_of_measurement = PERCENTAGE self._device_class = SensorDeviceClass.BATTERY self._state_class = SensorStateClass.MEASUREMENT @@ -149,7 +130,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a energy sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.ENERGY - self._units = UnitOfEnergy.KILO_WATT_HOUR + self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR self._device_class = SensorDeviceClass.ENERGY self._state_class = SensorStateClass.TOTAL @@ -172,7 +153,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a humidity sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.HUMIDITY - self._units = PERCENTAGE + self._attr_native_unit_of_measurement = PERCENTAGE self._device_class = SensorDeviceClass.HUMIDITY self._state_class = SensorStateClass.MEASUREMENT @@ -184,7 +165,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize an illuminance sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.ILLUMINANCE - self._units = LIGHT_LUX + self._attr_native_unit_of_measurement = LIGHT_LUX self._device_class = SensorDeviceClass.ILLUMINANCE self._state_class = SensorStateClass.MEASUREMENT @@ -196,7 +177,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a power sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.POWER - self._units = UnitOfPower.WATT + self._attr_native_unit_of_measurement = UnitOfPower.WATT self._device_class = SensorDeviceClass.POWER self._state_class = SensorStateClass.MEASUREMENT @@ -222,8 +203,10 @@ def __init__(self, *args: Any, **kwargs: Any): self._state_class = SensorStateClass.MEASUREMENT @property - def unit_of_measurement(self) -> str | None: - """Return the units for this sensor's value.""" + def native_unit_of_measurement(self) -> str | None: + unit = self.get_attr_unit(self._attribute) + if unit is not None: + return unit return ( UnitOfTemperature.FAHRENHEIT if self._hub.temperature_unit == TEMP_F @@ -231,24 +214,13 @@ def unit_of_measurement(self) -> str | None: ) -class HubitatDewPointSensor(HubitatSensor): +class HubitatDewPointSensor(HubitatTemperatureSensor): """A dewpoint sensor.""" def __init__(self, *args: Any, **kwargs: Any): """Initialize a dewpoint sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.DEW_POINT - self._device_class = SensorDeviceClass.TEMPERATURE - self._state_class = SensorStateClass.MEASUREMENT - - @property - def unit_of_measurement(self) -> str | None: - """Return the units for this sensor's value.""" - return ( - UnitOfTemperature.FAHRENHEIT - if self._hub.temperature_unit == TEMP_F - else UnitOfTemperature.CELSIUS - ) class HubitatVoltageSensor(HubitatSensor): @@ -258,7 +230,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a voltage sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.VOLTAGE - self._units = UnitOfElectricPotential.VOLT + self._attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT self._device_class = SensorDeviceClass.VOLTAGE self._state_class = SensorStateClass.MEASUREMENT @@ -274,7 +246,7 @@ def __init__(self, *args: Any, **kwargs: Any): # Maker API does not expose pressure unit # Override if necessary through customization.py # https://www.home-assistant.io/docs/configuration/customizing-devices/ - self._units = UnitOfPressure.MBAR + self._attr_native_unit_of_measurement = UnitOfPressure.MBAR self._device_class = SensorDeviceClass.PRESSURE self._state_class = SensorStateClass.MEASUREMENT @@ -286,7 +258,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a CarbonDioxide sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.CARBON_DIOXIDE - self._units = CONCENTRATION_PARTS_PER_MILLION + self._attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION self._device_class = SensorDeviceClass.CO2 self._state_class = SensorStateClass.MEASUREMENT @@ -298,7 +270,6 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a CarbonDioxideLevel sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.CARBON_DIOXIDE_LEVEL - self._units = None self._device_class = SensorDeviceClass.CO2 self._state_class = SensorStateClass.MEASUREMENT @@ -310,7 +281,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a CarbonMonoxide sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.CARBON_MONOXIDE - self._units = CONCENTRATION_PARTS_PER_MILLION + self._attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION self._device_class = SensorDeviceClass.CO self._state_class = SensorStateClass.MEASUREMENT @@ -322,7 +293,6 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a CarbonMonoxideLevel sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.CARBON_MONOXIDE_LEVEL - self._units = None self._device_class = SensorDeviceClass.CO self._state_class = SensorStateClass.MEASUREMENT @@ -334,7 +304,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a VOC sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.VOC - self._units = CONCENTRATION_PARTS_PER_BILLION + self._attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_BILLION self._device_class = SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS self._state_class = SensorStateClass.MEASUREMENT @@ -346,7 +316,6 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a VOC-Level sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.VOC_LEVEL - self._units = None self._device_class = SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS self._state_class = SensorStateClass.MEASUREMENT @@ -358,8 +327,6 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a HomeHealth sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.HOME_HEALTH - self._units = None - self._device_class = None self._state_class = SensorStateClass.MEASUREMENT @@ -370,7 +337,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a current sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.AMPERAGE - self._units = UnitOfElectricCurrent.AMPERE + self._attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE self._device_class = SensorDeviceClass.CURRENT self._state_class = SensorStateClass.MEASUREMENT @@ -393,7 +360,6 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize an AQI sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.AQI - self._units = "AQI" self._device_class = SensorDeviceClass.AQI self._state_class = SensorStateClass.MEASUREMENT @@ -405,7 +371,6 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize an airQualityIndex sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.AIR_QUALITY_INDEX - self._units = "AQI" self._device_class = SensorDeviceClass.AQI self._state_class = SensorStateClass.MEASUREMENT @@ -417,7 +382,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a PM1 sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.PM1 - self._units = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + self._attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER self._device_class = SensorDeviceClass.PM1 self._state_class = SensorStateClass.MEASUREMENT @@ -429,7 +394,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a PM10 sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.PM10 - self._units = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + self._attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER self._device_class = SensorDeviceClass.PM10 self._state_class = SensorStateClass.MEASUREMENT @@ -441,7 +406,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a PM2.5 sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.PM25 - self._units = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + self._attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER self._device_class = SensorDeviceClass.PM25 self._state_class = SensorStateClass.MEASUREMENT @@ -453,7 +418,9 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a rain rate sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.RAIN_RATE - self._units = UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR + self._attr_native_unit_of_measurement = ( + UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR + ) self._device_class = SensorDeviceClass.PRECIPITATION_INTENSITY self._state_class = SensorStateClass.MEASUREMENT @@ -465,7 +432,9 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a rain daily sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.RAIN_DAILY - self._units = UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR + self._attr_native_unit_of_measurement = ( + UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR + ) self._device_class = SensorDeviceClass.PRECIPITATION_INTENSITY self._state_class = SensorStateClass.MEASUREMENT @@ -477,7 +446,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a wind direction sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.WIND_DIRECTION - self._units = DEGREE + self._attr_native_unit_of_measurement = DEGREE self._device_class = SensorDeviceClass.WIND_SPEED self._state_class = SensorStateClass.MEASUREMENT @@ -489,7 +458,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a wind speed sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.WIND_SPEED - self._units = UnitOfSpeed.KILOMETERS_PER_HOUR + self._attr_native_unit_of_measurement = UnitOfSpeed.KILOMETERS_PER_HOUR self._device_class = SensorDeviceClass.WIND_SPEED self._state_class = SensorStateClass.MEASUREMENT @@ -501,7 +470,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a wind gust sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.WIND_GUST - self._units = UnitOfSpeed.KILOMETERS_PER_HOUR + self._attr_native_unit_of_measurement = UnitOfSpeed.KILOMETERS_PER_HOUR self._device_class = SensorDeviceClass.WIND_SPEED self._state_class = SensorStateClass.MEASUREMENT @@ -513,8 +482,6 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a rate sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.RATE - self._units = None - self._device_class = None self._state_class = SensorStateClass.MEASUREMENT @@ -525,8 +492,8 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a water day liter price sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.DAY_EURO - self._units = CURRENCY_EURO - self._device_class = NumberDeviceClass.MONETARY + self._attr_native_unit_of_measurement = CURRENCY_EURO + self._device_class = SensorDeviceClass.MONETARY self._state_class = SensorStateClass.MEASUREMENT @@ -537,7 +504,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a water day liter sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.DAY_LITER - self._units = UnitOfVolume.LITERS + self._attr_native_unit_of_measurement = UnitOfVolume.LITERS self._device_class = SensorDeviceClass.WATER self._state_class = SensorStateClass.MEASUREMENT @@ -549,7 +516,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a water cumulative liter sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.CUMULATIVE_LITER - self._units = UnitOfVolume.LITERS + self._attr_native_unit_of_measurement = UnitOfVolume.LITERS self._device_class = SensorDeviceClass.WATER self._state_class = SensorStateClass.TOTAL_INCREASING @@ -561,7 +528,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a water cumulative m3 sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.CUMULATIVE_CUBIC_METER - self._units = UnitOfVolume.CUBIC_METERS + self._attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS self._device_class = SensorDeviceClass.WATER self._state_class = SensorStateClass.MEASUREMENT @@ -573,7 +540,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize a water day m3 sensor.""" super().__init__(*args, **kwargs) self._attribute = DeviceAttribute.DAY_CUBIC_METER - self._units = UnitOfVolume.CUBIC_METERS + self._attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS self._device_class = SensorDeviceClass.WATER self._state_class = SensorStateClass.MEASUREMENT @@ -631,7 +598,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize an hsm status sensor.""" super().__init__(*args, **kwargs) self._attribute = ATTR_HSM_STATUS - self._device_class = DeviceType.HUB_HSM_STATUS + self._device_class = SensorDeviceClass.ENUM self._attribute_name = "HSM status" @@ -644,7 +611,7 @@ def __init__(self, *args: Any, **kwargs: Any): """Initialize an hsm status sensor.""" super().__init__(*args, **kwargs) self._attribute = ATTR_MODE - self._device_class = DeviceType.HUB_MODE + self._device_class = SensorDeviceClass.ENUM _SENSOR_ATTRS: tuple[tuple[str, Type[HubitatSensor]], ...] = ( @@ -736,7 +703,7 @@ def is_sensor(device: Device, overrides: dict[str, str] | None = None) -> bool: device=device, attribute=attr, enabled_default=False, - device_class="unknown", + device_class=None, ) ) _LOGGER.debug(f"Adding unknown entity for {device.id}:{attr}")