diff --git a/custom_components/tahoma/__init__.py b/custom_components/tahoma/__init__.py index 2febf5899..97dbff12d 100644 --- a/custom_components/tahoma/__init__.py +++ b/custom_components/tahoma/__init__.py @@ -8,6 +8,7 @@ from aiohttp import ClientError, ServerDisconnectedError from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.scene import DOMAIN as SCENE +from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_EXCLUDE, CONF_PASSWORD, CONF_SOURCE, CONF_USERNAME from homeassistant.core import HomeAssistant, ServiceCall @@ -208,6 +209,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Sensor and Binary Sensor will be added dynamically, based on the device states supported_platforms.add(BINARY_SENSOR) + supported_platforms.add(SENSOR) for platform in supported_platforms: hass.async_create_task( diff --git a/custom_components/tahoma/const.py b/custom_components/tahoma/const.py index 95f7ffde0..37d0232cf 100644 --- a/custom_components/tahoma/const.py +++ b/custom_components/tahoma/const.py @@ -4,7 +4,6 @@ from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.lock import DOMAIN as LOCK -from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.components.water_heater import DOMAIN as WATER_HEATER @@ -19,6 +18,21 @@ IGNORED_TAHOMA_DEVICES = [ "ProtocolGateway", "Pod", + # entries mapped to Sensor based on available states + "AirSensor", + "ConsumptionSensor", + "ElectricitySensor", + "GasSensor", + "GenericSensor", + "HumiditySensor", + "LightSensor", + "SunIntensitySensor", + "SunSensor", + "TemperatureSensor", + "ThermalEnergySensor", + "WaterSensor", + "WeatherSensor", + "WindSensor", # entries mapped to Binary Sensor based on available states "AirFlowSensor", # widgetName, uiClass is AirSensor (sensor) "ContactSensor", @@ -32,7 +46,6 @@ # Used to map the Somfy widget and ui_class to the Home Assistant platform TAHOMA_DEVICE_TO_PLATFORM = { "AdjustableSlatsRollerShutter": COVER, - "AirSensor": SENSOR, "Alarm": ALARM_CONTROL_PANEL, "AtlanticElectricalHeater": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) "AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) @@ -41,27 +54,21 @@ "AtlanticPassAPCHeatingAndCoolingZone": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) "AtlanticPassAPCZoneControl": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) "Awning": COVER, - "ConsumptionSensor": SENSOR, "Curtain": COVER, "DimmerExteriorHeating": CLIMATE, # widgetName, uiClass is ExteriorHeatingSystem (not supported) "DomesticHotWaterProduction": WATER_HEATER, # widgetName, uiClass is WaterHeatingSystem (not supported) "DomesticHotWaterTank": SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) "DoorLock": LOCK, - "ElectricitySensor": SENSOR, "EvoHomeController": CLIMATE, # widgetName, uiClass is EvoHome (not supported) "ExteriorScreen": COVER, "ExteriorVenetianBlind": COVER, "GarageDoor": COVER, - "GasSensor": SENSOR, "Gate": COVER, - "GenericSensor": SENSOR, "HeatingSetPoint": CLIMATE, # widgetName, uiClass is EvoHome (not supported) "HitachiDHW": WATER_HEATER, # widgetName, uiClass is HitachiHeatingSystem (not supported) "HitachiAirToWaterHeatingZone": CLIMATE, # widgetName, uiClass is HitachiHeatingSystem (not supported) "HitachiAirToAirHeatPump": CLIMATE, # widgetName, uiClass is HitachiHeatingSystem (not supported) - "HumiditySensor": SENSOR, "Light": LIGHT, - "LightSensor": SENSOR, "MyFoxSecurityCamera": COVER, # widgetName, uiClass is Camera (not supported) "OnOff": SWITCH, "Pergola": COVER, @@ -73,16 +80,9 @@ "SirenStatus": None, # widgetName, uiClass is Siren (switch) "SomfyThermostat": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) "StatelessExteriorHeating": CLIMATE, # widgetName, uiClass is ExteriorHeatingSystem. - "SunIntensitySensor": SENSOR, - "SunSensor": SENSOR, "SwimmingPool": SWITCH, "SwingingShutter": COVER, - "TemperatureSensor": SENSOR, - "ThermalEnergySensor": SENSOR, "VenetianBlind": COVER, - "WaterSensor": SENSOR, - "WeatherSensor": SENSOR, - "WindSensor": SENSOR, "Window": COVER, } diff --git a/custom_components/tahoma/cover_devices/tahoma_cover.py b/custom_components/tahoma/cover_devices/tahoma_cover.py index 75e774dfa..6cd044e77 100644 --- a/custom_components/tahoma/cover_devices/tahoma_cover.py +++ b/custom_components/tahoma/cover_devices/tahoma_cover.py @@ -61,12 +61,12 @@ CORE_TARGET_CLOSURE_STATE = "core:TargetClosureState" MYFOX_SHUTTER_STATUS_STATE = "myfox:ShutterStatusState" -ICON_LOCK_ALERT = "mdi:lock-alert" -ICON_WEATHER_WINDY = "mdi:weather-windy" - IO_PRIORITY_LOCK_LEVEL_STATE = "io:PriorityLockLevelState" IO_PRIORITY_LOCK_ORIGINATOR_STATE = "io:PriorityLockOriginatorState" +ICON_LOCK_ALERT = "mdi:lock-alert" +ICON_WEATHER_WINDY = "mdi:weather-windy" + STATE_CLOSED = "closed" SERVICE_COVER_MY_POSITION = "set_cover_my_position" @@ -128,19 +128,6 @@ def is_closed(self): return None - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - if ( - self.has_state(CORE_PRIORITY_LOCK_TIMER_STATE) - and self.select_state(CORE_PRIORITY_LOCK_TIMER_STATE) > 0 - ): - if self.select_state(IO_PRIORITY_LOCK_ORIGINATOR_STATE) == "wind": - return ICON_WEATHER_WINDY - return ICON_LOCK_ALERT - - return None - async def async_open_cover_tilt(self, **_): """Open the cover tilt.""" await self.async_execute_command(self.select_command(*COMMANDS_OPEN_TILT)) @@ -292,3 +279,16 @@ def supported_features(self): supported_features |= SUPPORT_MY return supported_features + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + if ( + self.has_state(CORE_PRIORITY_LOCK_TIMER_STATE) + and self.select_state(CORE_PRIORITY_LOCK_TIMER_STATE) > 0 + ): + if self.select_state(IO_PRIORITY_LOCK_ORIGINATOR_STATE) == "wind": + return ICON_WEATHER_WINDY + return ICON_LOCK_ALERT + + return None diff --git a/custom_components/tahoma/sensor.py b/custom_components/tahoma/sensor.py index e69cbcfbb..b2263b8fe 100644 --- a/custom_components/tahoma/sensor.py +++ b/custom_components/tahoma/sensor.py @@ -1,116 +1,313 @@ """Support for TaHoma sensors.""" +from __future__ import annotations + +from dataclasses import dataclass import logging -from typing import Optional +from typing import Any, Callable -from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.components import sensor +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, + LIGHT_LUX, PERCENTAGE, - POWER_KILO_WATT, POWER_WATT, - SPEED_METERS_PER_SECOND, + SIGNAL_STRENGTH_DECIBELS, TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_KELVIN, - VOLUME_CUBIC_METERS, + VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, VOLUME_LITERS, ) - -try: # Breaking change in 2021.8 - from homeassistant.const import ELECTRIC_CURRENT_AMPERE -except ImportError: - from homeassistant.const import ELECTRICAL_CURRENT_AMPERE as ELECTRIC_CURRENT_AMPERE - -try: # Breaking change in 2021.8 - from homeassistant.const import ELECTRIC_POTENTIAL_VOLT -except ImportError: - from homeassistant.const import VOLT as ELECTRIC_POTENTIAL_VOLT - from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.dt import utc_from_timestamp from .const import DOMAIN +from .coordinator import TahomaDataUpdateCoordinator from .tahoma_entity import TahomaEntity _LOGGER = logging.getLogger(__name__) -CORE_CO2_CONCENTRATION_STATE = "core:CO2ConcentrationState" -CORE_CO_CONCENTRATION_STATE = "core:COConcentrationState" -CORE_ELECTRIC_ENERGY_CONSUMPTION_STATE = "core:ElectricEnergyConsumptionState" -CORE_ELECTRIC_POWER_CONSUMPTION_STATE = "core:ElectricPowerConsumptionState" -CORE_FOSSIL_ENERGY_CONSUMPTION_STATE = "core:FossilEnergyConsumptionState" -CORE_GAS_CONSUMPTION_STATE = "core:GasConsumptionState" -CORE_LUMINANCE_STATE = "core:LuminanceState" -CORE_MEASURED_VALUE_TYPE = "core:MeasuredValueType" -CORE_RELATIVE_HUMIDITY_STATE = "core:RelativeHumidityState" -CORE_SUN_ENERGY_STATE = "core:SunEnergyState" -CORE_TEMPERATURE_STATE = "core:TemperatureState" -CORE_THERMAL_ENERGY_CONSUMPTION_STATE = "core:ThermalEnergyConsumptionState" -CORE_WATER_CONSUMPTION_STATE = "core:WaterConsumptionState" -CORE_WINDSPEED_STATE = "core:WindSpeedState" - - -DEVICE_CLASS_SUN_ENERGY = "sun_energy" -DEVICE_CLASS_WIND_SPEED = "wind_speed" - -ICON_MOLECULE_CO = "mdi:molecule-co" -ICON_MOLECULE_CO2 = "mdi:molecule-co2" -ICON_SOLAR_POWER = "mdi:solar-power" -ICON_WEATHER_WINDY = "mdi:weather-windy" - -UNIT_LX = "lx" - -TAHOMA_SENSOR_DEVICE_CLASSES = { - "CO2Sensor": DEVICE_CLASS_CO2, - "COSensor": DEVICE_CLASS_CO, - "ElectricitySensor": DEVICE_CLASS_POWER, - "HumiditySensor": DEVICE_CLASS_HUMIDITY, - "LightSensor": DEVICE_CLASS_ILLUMINANCE, - "RelativeHumiditySensor": DEVICE_CLASS_HUMIDITY, - "SunSensor": DEVICE_CLASS_SUN_ENERGY, - "TemperatureSensor": DEVICE_CLASS_TEMPERATURE, - "WindSensor": DEVICE_CLASS_WIND_SPEED, -} -# From https://www.tahomalink.com/enduser-mobile-web/steer-html5-client/tahoma/bootstrap.js -UNITS = { - "core:TemperatureInCelcius": TEMP_CELSIUS, - "core:TemperatureInCelsius": TEMP_CELSIUS, - "core:TemperatureInKelvin": TEMP_KELVIN, - "core:TemperatureInFahrenheit": TEMP_FAHRENHEIT, - "core:LuminanceInLux": UNIT_LX, - "core:ElectricCurrentInAmpere": ELECTRIC_CURRENT_AMPERE, - "core:VoltageInVolt": ELECTRIC_POTENTIAL_VOLT, - "core:ElectricalEnergyInWh": ENERGY_WATT_HOUR, - "core:ElectricalEnergyInKWh": ENERGY_KILO_WATT_HOUR, - "core:ElectricalEnergyInMWh": f"M{ENERGY_WATT_HOUR}", - "core:ElectricalPowerInW": POWER_WATT, - "core:ElectricalPowerInKW": POWER_KILO_WATT, - "core:ElectricalPowerInMW": f"M{POWER_WATT}", - "core:FlowInMeterCubePerHour": VOLUME_CUBIC_METERS, - "core:LinearSpeedInMeterPerSecond": SPEED_METERS_PER_SECOND, - "core:RelativeValueInPercentage": PERCENTAGE, - "core:VolumeInCubicMeter": VOLUME_CUBIC_METERS, - "core:VolumeInLiter": VOLUME_LITERS, - "core:FossilEnergyInWh": ENERGY_WATT_HOUR, - "core:FossilEnergyInKWh": ENERGY_KILO_WATT_HOUR, - "core:FossilEnergyInMWh": f"M{ENERGY_WATT_HOUR}", - "meters_seconds": SPEED_METERS_PER_SECOND, -} - -UNITS_BY_DEVICE_CLASS = { - DEVICE_CLASS_CO2: CONCENTRATION_PARTS_PER_MILLION, - DEVICE_CLASS_CO: CONCENTRATION_PARTS_PER_MILLION, -} + +@dataclass +class OverkizSensorDescription(SensorEntityDescription): + """Class to describe a Overkiz sensor.""" + + value: Callable[[Any], Any] | None = lambda val: val + + +SENSOR_DESCRIPTIONS = [ + OverkizSensorDescription( + key="core:BatteryLevelState", + name="Battery Level", + unit_of_measurement=PERCENTAGE, + device_class=sensor.DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="core:BatteryState", + name="Battery", + device_class=sensor.DEVICE_CLASS_BATTERY, + value=lambda value: str(value).capitalize(), + ), + OverkizSensorDescription( + key="core:RSSILevelState", + name="RSSI Level", + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + device_class=sensor.DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + value=lambda value: round(value), + ), + OverkizSensorDescription( + key="core:ExpectedNumberOfShowerState", + name="Expected Number Of Shower", + icon="mdi:shower-head", + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="core:NumberOfShowerRemainingState", + name="Number of Shower Remaining", + icon="mdi:shower-head", + state_class=STATE_CLASS_MEASUREMENT, + ), + # V40 is measured in litres (L) and shows the amount of warm (mixed) water with a temperature of 40 C, which can be drained from a switched off electric water heater. + OverkizSensorDescription( + key="core:V40WaterVolumeEstimationState", + name="Water Volume Estimation at 40 °C", + icon="mdi:water", + unit_of_measurement=VOLUME_LITERS, + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="core:WaterConsumptionState", + name="Water Consumption", + icon="mdi:water", + unit_of_measurement=VOLUME_LITERS, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="io:OutletEngineState", + name="Outlet Engine", + icon="mdi:fan-chevron-down", + unit_of_measurement=VOLUME_LITERS, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="io:InletEngineState", + name="Inlet Engine", + icon="mdi:fan-chevron-up", + unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="hlrrwifi:RoomTemperatureState", + name="Room Temperature", + device_class=sensor.DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="io:MiddleWaterTemperatureState", + name="Middle Water Temperature", + device_class=sensor.DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="io:PriorityLockOriginatorState", + name="Priority Lock Originator", + icon="mdi:alert", + ), + OverkizSensorDescription( + key="core:FossilEnergyConsumptionState", + name="Fossil Energy Consumption", + device_class=sensor.DEVICE_CLASS_ENERGY, + ), + OverkizSensorDescription( + key="core:GasConsumptionState", + name="Gas Consumption", + ), + OverkizSensorDescription( + key="core:ThermalEnergyConsumptionState", + name="Thermal Energy Consumption", + ), + # LightSensor/LuminanceSensor + OverkizSensorDescription( + key="core:LuminanceState", + name="Luminance", + device_class=sensor.DEVICE_CLASS_ILLUMINANCE, + unit_of_measurement=LIGHT_LUX, # core:MeasuredValueType = core:LuminanceInLux + state_class=STATE_CLASS_MEASUREMENT, + ), + # ElectricitySensor/CumulativeElectricPowerConsumptionSensor + OverkizSensorDescription( + key="core:ElectricEnergyConsumptionState", + name="Electric Energy Consumption", + device_class=sensor.DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh (not for modbus:YutakiV2DHWElectricalEnergyConsumptionComponent) + state_class=STATE_CLASS_MEASUREMENT, # core:MeasurementCategory attribute = electric/overall + last_reset=utc_from_timestamp(0), + ), + OverkizSensorDescription( + key="core:ElectricPowerConsumptionState", + name="Electric Power Consumption", + device_class=sensor.DEVICE_CLASS_POWER, + unit_of_measurement=POWER_WATT, # core:MeasuredValueType = core:ElectricalEnergyInWh (not for modbus:YutakiV2DHWElectricalEnergyConsumptionComponent) + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="core:ConsumptionTariff1State", + name="Consumption Tariff 1", + device_class=sensor.DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="core:ConsumptionTariff2State", + name="Consumption Tariff 2", + device_class=sensor.DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="core:ConsumptionTariff3State", + name="Consumption Tariff 3", + device_class=sensor.DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="core:ConsumptionTariff4State", + name="Consumption Tariff 4", + device_class=sensor.DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="core:ConsumptionTariff5State", + name="Consumption Tariff 5", + device_class=sensor.DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="core:ConsumptionTariff6State", + name="Consumption Tariff 6", + device_class=sensor.DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="core:ConsumptionTariff7State", + name="Consumption Tariff 7", + device_class=sensor.DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="core:ConsumptionTariff8State", + name="Consumption Tariff 8", + device_class=sensor.DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="core:ConsumptionTariff9State", + name="Consumption Tariff 9", + device_class=sensor.DEVICE_CLASS_ENERGY, + unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh + entity_registry_enabled_default=False, + state_class=STATE_CLASS_MEASUREMENT, + ), + # HumiditySensor/RelativeHumiditySensor + OverkizSensorDescription( + key="core:RelativeHumidityState", + name="Relative Humidity", + value=lambda value: round(value, 2), + device_class=sensor.DEVICE_CLASS_HUMIDITY, + unit_of_measurement=PERCENTAGE, # core:MeasuredValueType = core:RelativeValueInPercentage + state_class=STATE_CLASS_MEASUREMENT, + ), + # TemperatureSensor/TemperatureSensor + OverkizSensorDescription( + key="core:TemperatureState", + name="Temperature", + device_class=sensor.DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=TEMP_CELSIUS, # core:MeasuredValueType = core:TemperatureInCelcius + state_class=STATE_CLASS_MEASUREMENT, + ), + # WeatherSensor/WeatherForecastSensor + OverkizSensorDescription( + key="core:WeatherStatusState", + name="Weather Status", + ), + OverkizSensorDescription( + key="core:MinimumTemperatureState", + name="Minimum Temperature", + device_class=sensor.DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=TEMP_CELSIUS, + state_class=STATE_CLASS_MEASUREMENT, + ), + OverkizSensorDescription( + key="core:MaximumTemperatureState", + name="Maximum Temperature", + device_class=sensor.DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=TEMP_CELSIUS, + state_class=STATE_CLASS_MEASUREMENT, + ), + # AirSensor/COSensor + OverkizSensorDescription( + key="core:COConcentrationState", + name="CO Concentration", + device_class=sensor.DEVICE_CLASS_CO, + unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=STATE_CLASS_MEASUREMENT, + ), + # AirSensor/CO2Sensor + OverkizSensorDescription( + key="core:CO2ConcentrationState", + name="CO2 Concentration", + device_class=sensor.DEVICE_CLASS_CO2, + unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=STATE_CLASS_MEASUREMENT, + ), + # SunSensor/SunEnergySensor + OverkizSensorDescription( + key="core:SunEnergyState", + name="Sun Energy", + value=lambda value: round(value, 2), + device_class=sensor.DEVICE_CLASS_ENERGY, + icon="mdi:solar-power", + state_class=STATE_CLASS_MEASUREMENT, + ), + # WindSensor/WindSpeedSensor + OverkizSensorDescription( + key="core:WindSpeedState", + name="Wind Speed", + value=lambda value: round(value, 2), + icon="mdi:weather-windy", + state_class=STATE_CLASS_MEASUREMENT, + ), + # SmokeSensor/SmokeSensor + OverkizSensorDescription( + key="io:SensorRoomState", + name="Sensor Room", + value=lambda value: str(value).capitalize(), + entity_registry_enabled_default=False, + ), +] async def async_setup_entry( @@ -122,68 +319,59 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] coordinator = data["coordinator"] - entities = [ - TahomaSensor(device.deviceurl, coordinator) - for device in data["platforms"][SENSOR] - if device.states - ] + entities = [] + + key_supported_states = { + description.key: description for description in SENSOR_DESCRIPTIONS + } + + for device in coordinator.data.values(): + for state in device.states: + description = key_supported_states.get(state.name) + if description: + entities.append( + TahomaStateSensor( + device.deviceurl, + coordinator, + description, + ) + ) async_add_entities(entities) -class TahomaSensor(TahomaEntity, Entity): +class TahomaStateSensor(TahomaEntity, SensorEntity): """Representation of a TaHoma Sensor.""" + def __init__( + self, + device_url: str, + coordinator: TahomaDataUpdateCoordinator, + description: OverkizSensorDescription, + ): + """Initialize the device.""" + super().__init__(device_url, coordinator) + self.entity_description = description + @property def state(self): """Return the value of the sensor.""" - state = self.select_state( - CORE_CO2_CONCENTRATION_STATE, - CORE_CO_CONCENTRATION_STATE, - CORE_ELECTRIC_ENERGY_CONSUMPTION_STATE, - CORE_ELECTRIC_POWER_CONSUMPTION_STATE, - CORE_FOSSIL_ENERGY_CONSUMPTION_STATE, - CORE_GAS_CONSUMPTION_STATE, - CORE_LUMINANCE_STATE, - CORE_RELATIVE_HUMIDITY_STATE, - CORE_SUN_ENERGY_STATE, - CORE_TEMPERATURE_STATE, - CORE_THERMAL_ENERGY_CONSUMPTION_STATE, - CORE_WINDSPEED_STATE, - CORE_WATER_CONSUMPTION_STATE, - ) - return round(state, 2) if state is not None else None + state = self.device.states[self.entity_description.key] - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - if ( - self.device.attributes - and CORE_MEASURED_VALUE_TYPE in self.device.attributes - ): - attribute = self.device.attributes[CORE_MEASURED_VALUE_TYPE] - return UNITS.get(attribute.value) - - if self.device_class in UNITS_BY_DEVICE_CLASS: - return UNITS_BY_DEVICE_CLASS.get(self.device_class) + # Transform the value with a lambda function + if hasattr(self.entity_description, "value"): + return self.entity_description.value(state.value) - return None + return state.value @property - def icon(self) -> Optional[str]: - """Return the icon to use in the frontend, if any.""" - icons = { - DEVICE_CLASS_CO: ICON_MOLECULE_CO, - DEVICE_CLASS_CO2: ICON_MOLECULE_CO2, - DEVICE_CLASS_WIND_SPEED: ICON_WEATHER_WINDY, - DEVICE_CLASS_SUN_ENERGY: ICON_SOLAR_POWER, - } - - return icons.get(self.device_class) + def name(self) -> str: + """Return the name of the device.""" + if self.index: + return f"{self.entity_description.name} {self.index}" + return self.entity_description.name @property - def device_class(self) -> Optional[str]: - """Return the device class of this entity if any.""" - return TAHOMA_SENSOR_DEVICE_CLASSES.get( - self.device.widget - ) or TAHOMA_SENSOR_DEVICE_CLASSES.get(self.device.ui_class) + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{super().unique_id}-{self.entity_description.key}" diff --git a/custom_components/tahoma/tahoma_entity.py b/custom_components/tahoma/tahoma_entity.py index d5458964d..d4fc39e65 100644 --- a/custom_components/tahoma/tahoma_entity.py +++ b/custom_components/tahoma/tahoma_entity.py @@ -11,20 +11,17 @@ from .const import DOMAIN from .coordinator import TahomaDataUpdateCoordinator -ATTR_RSSI_LEVEL = "rssi_level" - -CORE_AVAILABILITY_STATE = "core:AvailabilityState" -CORE_BATTERY_STATE = "core:BatteryState" CORE_MANUFACTURER = "core:Manufacturer" CORE_MANUFACTURER_NAME_STATE = "core:ManufacturerNameState" CORE_MODEL_STATE = "core:ModelState" CORE_PRODUCT_MODEL_NAME_STATE = "core:ProductModelNameState" -CORE_RSSI_LEVEL_STATE = "core:RSSILevelState" -CORE_SENSOR_DEFECT_STATE = "core:SensorDefectState" -CORE_STATUS_STATE = "core:StatusState" IO_MODEL_STATE = "io:ModelState" +# To be removed when this is implemented in sensor/binary sensor +CORE_BATTERY_STATE = "core:BatteryState" +CORE_SENSOR_DEFECT_STATE = "core:SensorDefectState" + STATE_AVAILABLE = "available" STATE_BATTERY_FULL = "full" STATE_BATTERY_NORMAL = "normal" @@ -49,7 +46,8 @@ def __init__(self, device_url: str, coordinator: TahomaDataUpdateCoordinator): """Initialize the device.""" super().__init__(coordinator) self.device_url = device_url - self.base_device_url = self.get_base_device_url() + self.base_device_url, *index = self.device_url.split("#") + self.index = index[0] if index else None @property def device(self) -> Device: @@ -81,9 +79,6 @@ def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the device.""" attr = {} - if self.has_state(CORE_RSSI_LEVEL_STATE): - attr[ATTR_RSSI_LEVEL] = self.select_state(CORE_RSSI_LEVEL_STATE) - if self.has_state(CORE_BATTERY_STATE): battery_state = self.select_state(CORE_BATTERY_STATE) attr[ATTR_BATTERY_LEVEL] = BATTERY_MAP.get(battery_state, battery_state) @@ -128,7 +123,7 @@ def device_info(self) -> Dict[str, Any]: return { "identifiers": {(DOMAIN, self.base_device_url)}, - "name": self.name, + "name": self.device.label, "manufacturer": manufacturer, "model": model, "sw_version": self.device.controllable_name, @@ -199,14 +194,6 @@ async def async_cancel_command(self, exec_id: str): """Cancel device command in async context.""" await self.coordinator.client.cancel_command(exec_id) - def get_base_device_url(self): - """Return base device url.""" - if "#" not in self.device_url: - return self.device_url - - device_url, _ = self.device_url.split("#") - return device_url - def get_gateway_id(self): """Retrieve gateway id from device url.""" result = re.search(r":\/\/(.*)\/", self.device_url) diff --git a/hacs.json b/hacs.json index 820d74eca..56a34d54f 100644 --- a/hacs.json +++ b/hacs.json @@ -8,7 +8,7 @@ "switch", "climate" ], - "homeassistant": "2021.7.0", + "homeassistant": "2021.8.0", "render_readme": "true", "iot_class": "Cloud Polling" } \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt index f5e6d8b03..646598427 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,2 +1,2 @@ -r requirements.txt -homeassistant==2021.7.0b0 \ No newline at end of file +homeassistant==2021.8.0b0 \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index dc1e96479..9e62c269f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,2 +1,2 @@ -r requirements_dev.txt -pytest-homeassistant-custom-component==0.4.2 \ No newline at end of file +pytest-homeassistant-custom-component==0.4.3 \ No newline at end of file