From 58f6c9926e58927072158eeac73b59c2959c59cc Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Wed, 4 Aug 2021 21:07:51 +0200 Subject: [PATCH 1/6] Refactor a lot... --- .../tahoma/alarm_control_panel.py | 28 +-- custom_components/tahoma/binary_sensor.py | 6 +- .../atlantic_electrical_heater.py | 10 +- ...er_with_adjustable_temperature_setpoint.py | 16 +- .../atlantic_electrical_towel_dryer.py | 18 +- ...antic_pass_apc_heating_and_cooling_zone.py | 16 +- .../atlantic_pass_apc_zone_control.py | 8 +- .../climate_devices/atlantic_pass_apcdhw.py | 20 +- .../dimmer_exterior_heating.py | 10 +- .../climate_devices/evo_home_controller.py | 8 +- .../climate_devices/heating_set_point.py | 8 +- .../hitachi_air_to_air_heat_pump.py | 6 +- .../hitachi_air_to_water_heating_zone.py | 18 +- .../climate_devices/somfy_thermostat.py | 22 +- .../stateless_exterior_heating.py | 4 +- custom_components/tahoma/cover.py | 5 +- .../tahoma/cover_devices/awning.py | 2 +- .../tahoma/cover_devices/tahoma_cover.py | 24 +- .../tahoma/cover_devices/vertical_cover.py | 6 +- custom_components/tahoma/entity.py | 138 +++++++++++ custom_components/tahoma/executor.py | 104 +++++++++ custom_components/tahoma/light.py | 36 +-- custom_components/tahoma/lock.py | 10 +- custom_components/tahoma/sensor.py | 6 +- custom_components/tahoma/switch.py | 9 +- custom_components/tahoma/tahoma_entity.py | 217 ------------------ .../domestic_hot_water_production.py | 21 +- .../water_heater_devices/hitachi_dhw.py | 14 +- 28 files changed, 425 insertions(+), 365 deletions(-) create mode 100644 custom_components/tahoma/entity.py create mode 100644 custom_components/tahoma/executor.py delete mode 100644 custom_components/tahoma/tahoma_entity.py diff --git a/custom_components/tahoma/alarm_control_panel.py b/custom_components/tahoma/alarm_control_panel.py index 928650aeb..fbbe0bdc1 100644 --- a/custom_components/tahoma/alarm_control_panel.py +++ b/custom_components/tahoma/alarm_control_panel.py @@ -25,7 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .tahoma_entity import TahomaEntity +from .entity import OverkizEntity COMMAND_ALARM_OFF = "alarmOff" COMMAND_ALARM_ON = "alarmOn" @@ -95,14 +95,14 @@ async def async_setup_entry( async_add_entities(entities) -class TahomaAlarmControlPanel(TahomaEntity, AlarmControlPanelEntity): +class TahomaAlarmControlPanel(OverkizEntity, AlarmControlPanelEntity): """Representation of a TaHoma Alarm Control Panel.""" @property def state(self): """Return the state of the device.""" if self.has_state(CORE_INTRUSION_STATE, INTERNAL_INTRUSION_DETECTED_STATE): - state = self.select_state( + state = self.executor.select_state( CORE_INTRUSION_STATE, INTERNAL_INTRUSION_DETECTED_STATE ) if state == STATE_DETECTED: @@ -113,22 +113,24 @@ def state(self): if ( self.has_state(INTERNAL_CURRENT_ALARM_MODE_STATE) and self.has_state(INTERNAL_TARGET_ALARM_MODE_STATE) - and self.select_state(INTERNAL_CURRENT_ALARM_MODE_STATE) - != self.select_state(INTERNAL_TARGET_ALARM_MODE_STATE) + and self.executor.select_state(INTERNAL_CURRENT_ALARM_MODE_STATE) + != self.executor.select_state(INTERNAL_TARGET_ALARM_MODE_STATE) ): return STATE_ALARM_PENDING if self.has_state(MYFOX_ALARM_STATUS_STATE): - return MAP_MYFOX_STATUS_STATE[self.select_state(MYFOX_ALARM_STATUS_STATE)] + return MAP_MYFOX_STATUS_STATE[ + self.executor.select_state(MYFOX_ALARM_STATUS_STATE) + ] if self.has_state(INTERNAL_CURRENT_ALARM_MODE_STATE): return MAP_INTERNAL_STATUS_STATE[ - self.select_state(INTERNAL_CURRENT_ALARM_MODE_STATE) + self.executor.select_state(INTERNAL_CURRENT_ALARM_MODE_STATE) ] if self.has_state(VERISURE_ALARM_PANEL_MAIN_ARM_TYPE_STATE): return MAP_VERISURE_STATUS_STATE[ - self.select_state(VERISURE_ALARM_PANEL_MAIN_ARM_TYPE_STATE) + self.executor.select_state(VERISURE_ALARM_PANEL_MAIN_ARM_TYPE_STATE) ] return None @@ -158,7 +160,7 @@ def supported_features(self) -> int: async def async_alarm_disarm(self, code=None): """Send disarm command.""" await self.async_execute_command( - self.select_command(COMMAND_DISARM, COMMAND_ALARM_OFF) + self.executor.select_command(COMMAND_DISARM, COMMAND_ALARM_OFF) ) async def async_alarm_arm_home(self, code=None): @@ -170,7 +172,7 @@ async def async_alarm_arm_home(self, code=None): async def async_alarm_arm_night(self, code=None): """Send arm night command.""" await self.async_execute_command( - self.select_command( + self.executor.select_command( COMMAND_PARTIAL, COMMAND_ALARM_PARTIAL_2, COMMAND_ARM_PARTIAL_NIGHT ) ) @@ -178,19 +180,19 @@ async def async_alarm_arm_night(self, code=None): async def async_alarm_arm_away(self, code=None): """Send arm away command.""" await self.async_execute_command( - self.select_command(COMMAND_ARM, COMMAND_ALARM_ON) + self.executor.select_command(COMMAND_ARM, COMMAND_ALARM_ON) ) async def async_alarm_trigger(self, code=None) -> None: """Send alarm trigger command.""" await self.async_execute_command( - self.select_command(COMMAND_SET_ALARM_STATUS, STATE_DETECTED) + self.executor.select_command(COMMAND_SET_ALARM_STATUS, STATE_DETECTED) ) async def async_alarm_arm_custom_bypass(self, code=None) -> None: """Send arm custom bypass command.""" await self.async_execute_command( - self.select_command(COMMAND_SET_ALARM_STATUS, STATE_UNDETECTED) + self.executor.select_command(COMMAND_SET_ALARM_STATUS, STATE_UNDETECTED) ) @property diff --git a/custom_components/tahoma/binary_sensor.py b/custom_components/tahoma/binary_sensor.py index 8e696ac6a..a9ea574fa 100644 --- a/custom_components/tahoma/binary_sensor.py +++ b/custom_components/tahoma/binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .tahoma_entity import TahomaEntity +from .tahoma_entity import OverkizEntity CORE_ASSEMBLY_STATE = "core:AssemblyState" CORE_BUTTON_STATE = "core:ButtonState" @@ -77,7 +77,7 @@ async def async_setup_entry( async_add_entities(entities) -class TahomaBinarySensor(TahomaEntity, BinarySensorEntity): +class TahomaBinarySensor(OverkizEntity, BinarySensorEntity): """Representation of a TaHoma Binary Sensor.""" @property @@ -85,7 +85,7 @@ def is_on(self): """Return the state of the sensor.""" return ( - self.select_state( + self.executor.select_state( CORE_ASSEMBLY_STATE, CORE_BUTTON_STATE, CORE_CONTACT_STATE, diff --git a/custom_components/tahoma/climate_devices/atlantic_electrical_heater.py b/custom_components/tahoma/climate_devices/atlantic_electrical_heater.py index 4dc32e7a3..158bee9c6 100644 --- a/custom_components/tahoma/climate_devices/atlantic_electrical_heater.py +++ b/custom_components/tahoma/climate_devices/atlantic_electrical_heater.py @@ -14,7 +14,7 @@ ) from homeassistant.const import TEMP_CELSIUS -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity COMMAND_SET_HEATING_LEVEL = "setHeatingLevel" @@ -44,7 +44,7 @@ HVAC_MODES_TO_TAHOMA = {v: k for k, v in TAHOMA_TO_HVAC_MODES.items()} -class AtlanticElectricalHeater(TahomaEntity, ClimateEntity): +class AtlanticElectricalHeater(OverkizEntity, ClimateEntity): """Representation of Atlantic Electrical Heater.""" @property @@ -60,7 +60,7 @@ def supported_features(self) -> int: @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" - return TAHOMA_TO_HVAC_MODES[self.select_state(CORE_ON_OFF_STATE)] + return TAHOMA_TO_HVAC_MODES[self.executor.select_state(CORE_ON_OFF_STATE)] @property def hvac_modes(self) -> List[str]: @@ -76,7 +76,9 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" - return TAHOMA_TO_PRESET_MODES[self.select_state(IO_TARGET_HEATING_LEVEL_STATE)] + return TAHOMA_TO_PRESET_MODES[ + self.executor.select_state(IO_TARGET_HEATING_LEVEL_STATE) + ] @property def preset_modes(self) -> Optional[List[str]]: diff --git a/custom_components/tahoma/climate_devices/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py b/custom_components/tahoma/climate_devices/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py index d2ea2e6ed..f53da3c9d 100644 --- a/custom_components/tahoma/climate_devices/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py +++ b/custom_components/tahoma/climate_devices/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py @@ -26,7 +26,7 @@ from homeassistant.helpers.event import async_track_state_change from ..coordinator import TahomaDataUpdateCoordinator -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity _LOGGER = logging.getLogger(__name__) @@ -80,7 +80,7 @@ class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint( - TahomaEntity, ClimateEntity + OverkizEntity, ClimateEntity ): """Representation of Atlantic Electrical Heater (With Adjustable Temperature Setpoint).""" @@ -181,9 +181,11 @@ def hvac_modes(self) -> List[str]: def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" if CORE_OPERATING_MODE_STATE in self.device.states: - return TAHOMA_TO_HVAC_MODE[self.select_state(CORE_OPERATING_MODE_STATE)] + return TAHOMA_TO_HVAC_MODE[ + self.executor.select_state(CORE_OPERATING_MODE_STATE) + ] if CORE_ON_OFF_STATE in self.device.states: - return TAHOMA_TO_HVAC_MODE[self.select_state(CORE_ON_OFF_STATE)] + return TAHOMA_TO_HVAC_MODE[self.executor.select_state(CORE_ON_OFF_STATE)] async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" @@ -209,7 +211,9 @@ def preset_modes(self) -> Optional[List[str]]: @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" - return TAHOMA_TO_PRESET_MODE[self.select_state(IO_TARGET_HEATING_LEVEL_STATE)] + return TAHOMA_TO_PRESET_MODE[ + self.executor.select_state(IO_TARGET_HEATING_LEVEL_STATE) + ] async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" @@ -226,7 +230,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: def target_temperature(self) -> None: """Return the temperature.""" if CORE_TARGET_TEMPERATURE_STATE in self.device.states: - return self.select_state(CORE_TARGET_TEMPERATURE_STATE) + return self.executor.select_state(CORE_TARGET_TEMPERATURE_STATE) @property def current_temperature(self): diff --git a/custom_components/tahoma/climate_devices/atlantic_electrical_towel_dryer.py b/custom_components/tahoma/climate_devices/atlantic_electrical_towel_dryer.py index 529ad2283..2cd7daa8c 100644 --- a/custom_components/tahoma/climate_devices/atlantic_electrical_towel_dryer.py +++ b/custom_components/tahoma/climate_devices/atlantic_electrical_towel_dryer.py @@ -15,7 +15,7 @@ ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity _LOGGER = logging.getLogger(__name__) @@ -63,7 +63,7 @@ HVAC_MODE_TO_TAHOMA = {v: k for k, v in TAHOMA_TO_HVAC_MODE.items()} -class AtlanticElectricalTowelDryer(TahomaEntity, ClimateEntity): +class AtlanticElectricalTowelDryer(OverkizEntity, ClimateEntity): """Representation of Atlantic Electrical Towel Dryer.""" @property @@ -85,10 +85,12 @@ def hvac_modes(self) -> List[str]: def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" if CORE_OPERATING_MODE_STATE in self.device.states: - return TAHOMA_TO_HVAC_MODE[self.select_state(CORE_OPERATING_MODE_STATE)] + return TAHOMA_TO_HVAC_MODE[ + self.executor.select_state(CORE_OPERATING_MODE_STATE) + ] if CORE_ON_OFF_STATE in self.device.states: - return TAHOMA_TO_HVAC_MODE[self.select_state(CORE_ON_OFF_STATE)] + return TAHOMA_TO_HVAC_MODE[self.executor.select_state(CORE_ON_OFF_STATE)] async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" @@ -105,7 +107,7 @@ def preset_modes(self) -> Optional[List[str]]: def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" return TAHOMA_TO_PRESET_MODE[ - self.select_state(IO_TOWEL_DRYER_TEMPORARY_STATE_STATE) + self.executor.select_state(IO_TOWEL_DRYER_TEMPORARY_STATE_STATE) ] async def async_set_preset_mode(self, preset_mode: str) -> None: @@ -118,14 +120,14 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: def target_temperature(self) -> None: """Return the temperature.""" if self.hvac_mode == HVAC_MODE_AUTO: - return self.select_state(IO_EFFECTIVE_TEMPERATURE_SETPOINT_STATE) + return self.executor.select_state(IO_EFFECTIVE_TEMPERATURE_SETPOINT_STATE) else: - return self.select_state(CORE_TARGET_TEMPERATURE_STATE) + return self.executor.select_state(CORE_TARGET_TEMPERATURE_STATE) @property def current_temperature(self): """Return current temperature.""" - return self.select_state(CORE_COMFORT_ROOM_TEMPERATURE_STATE) + return self.executor.select_state(CORE_COMFORT_ROOM_TEMPERATURE_STATE) async def async_set_temperature(self, **kwargs) -> None: """Set new temperature.""" diff --git a/custom_components/tahoma/climate_devices/atlantic_pass_apc_heating_and_cooling_zone.py b/custom_components/tahoma/climate_devices/atlantic_pass_apc_heating_and_cooling_zone.py index e349a4d67..50d28d9d5 100644 --- a/custom_components/tahoma/climate_devices/atlantic_pass_apc_heating_and_cooling_zone.py +++ b/custom_components/tahoma/climate_devices/atlantic_pass_apc_heating_and_cooling_zone.py @@ -19,7 +19,7 @@ from homeassistant.helpers.event import async_track_state_change from ..coordinator import TahomaDataUpdateCoordinator -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity _LOGGER = logging.getLogger(__name__) @@ -59,7 +59,7 @@ HVAC_MODE_TO_TAHOMA = {v: k for k, v in TAHOMA_TO_HVAC_MODE.items()} -class AtlanticPassAPCHeatingAndCoolingZone(TahomaEntity, ClimateEntity): +class AtlanticPassAPCHeatingAndCoolingZone(OverkizEntity, ClimateEntity): """Representation of Atlantic Pass APC Heating and Cooling Zone.""" def __init__(self, device_url: str, coordinator: TahomaDataUpdateCoordinator): @@ -132,12 +132,12 @@ def update_temp(self, state): @property def min_temp(self) -> float: """Return the minimum temperature.""" - return self.select_state(CORE_MINIMUM_HEATING_TARGET_TEMPERATURE_STATE) + return self.executor.select_state(CORE_MINIMUM_HEATING_TARGET_TEMPERATURE_STATE) @property def max_temp(self) -> float: """Return the maximum temperature.""" - return self.select_state(CORE_MAXIMUM_HEATING_TARGET_TEMPERATURE_STATE) + return self.executor.select_state(CORE_MAXIMUM_HEATING_TARGET_TEMPERATURE_STATE) @property def current_temperature(self) -> Optional[float]: @@ -166,10 +166,12 @@ def hvac_modes(self) -> List[str]: def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" - if self.select_state(CORE_HEATING_ON_OFF_STATE) == "off": + if self.executor.select_state(CORE_HEATING_ON_OFF_STATE) == "off": return HVAC_MODE_OFF - return TAHOMA_TO_HVAC_MODE[self.select_state(IO_PASS_APC_HEATING_MODE_STATE)] + return TAHOMA_TO_HVAC_MODE[ + self.executor.select_state(IO_PASS_APC_HEATING_MODE_STATE) + ] async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" @@ -189,7 +191,7 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: @property def target_temperature(self) -> None: """Return the temperature.""" - return self.select_state(CORE_HEATING_TARGET_TEMPERATURE_STATE) + return self.executor.select_state(CORE_HEATING_TARGET_TEMPERATURE_STATE) async def async_set_temperature(self, **kwargs) -> None: """Set new temperature.""" diff --git a/custom_components/tahoma/climate_devices/atlantic_pass_apc_zone_control.py b/custom_components/tahoma/climate_devices/atlantic_pass_apc_zone_control.py index 4b1b6444a..832585b7f 100644 --- a/custom_components/tahoma/climate_devices/atlantic_pass_apc_zone_control.py +++ b/custom_components/tahoma/climate_devices/atlantic_pass_apc_zone_control.py @@ -11,7 +11,7 @@ ) from homeassistant.const import TEMP_CELSIUS -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity _LOGGER = logging.getLogger(__name__) @@ -30,7 +30,7 @@ HVAC_MODE_TO_TAHOMA = {v: k for k, v in TAHOMA_TO_HVAC_MODE.items()} -class AtlanticPassAPCZoneControl(TahomaEntity, ClimateEntity): +class AtlanticPassAPCZoneControl(OverkizEntity, ClimateEntity): """Representation of Atlantic Pass APC Zone Control.""" @property @@ -53,7 +53,9 @@ def hvac_modes(self) -> List[str]: @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" - return TAHOMA_TO_HVAC_MODE[self.select_state(IO_PASS_APC_OPERATING_MODE_STATE)] + return TAHOMA_TO_HVAC_MODE[ + self.executor.select_state(IO_PASS_APC_OPERATING_MODE_STATE) + ] async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" diff --git a/custom_components/tahoma/climate_devices/atlantic_pass_apcdhw.py b/custom_components/tahoma/climate_devices/atlantic_pass_apcdhw.py index 580fd749d..a1f50ea73 100644 --- a/custom_components/tahoma/climate_devices/atlantic_pass_apcdhw.py +++ b/custom_components/tahoma/climate_devices/atlantic_pass_apcdhw.py @@ -15,7 +15,7 @@ ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity BOOST_ON_STATE = "on" BOOST_OFF_STATE = "off" @@ -70,7 +70,7 @@ MAP_REVERSE_PRESET_MODES = {v: k for k, v in MAP_PRESET_MODES.items()} -class AtlanticPassAPCDHW(TahomaEntity, ClimateEntity): +class AtlanticPassAPCDHW(OverkizEntity, ClimateEntity): """Representation of TaHoma IO Atlantic Electrical Heater.""" @property @@ -96,14 +96,16 @@ def max_temp(self) -> float: @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" - if self.select_state(IO_PASS_APCDWH_MODE_STATE) in [ + if self.executor.select_state(IO_PASS_APCDWH_MODE_STATE) in [ PASS_APCDHW_MODE_ECO, PASS_APCDWH_MODE_INTERNAL_SCHEDULING, PASS_APCDHW_MODE_STOP, ]: - return MAP_PRESET_MODES[self.select_state(IO_PASS_APCDWH_MODE_STATE)] + return MAP_PRESET_MODES[ + self.executor.select_state(IO_PASS_APCDWH_MODE_STATE) + ] - if self.select_state(CORE_BOOST_ON_OFF_STATE) == BOOST_ON_STATE: + if self.executor.select_state(CORE_BOOST_ON_OFF_STATE) == BOOST_ON_STATE: return PRESET_BOOST return PRESET_COMFORT @@ -138,7 +140,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" - return MAP_HVAC_MODES[self.select_state(CORE_DWH_ON_OFF_STATE)] + return MAP_HVAC_MODES[self.executor.select_state(CORE_DWH_ON_OFF_STATE)] @property def hvac_modes(self) -> List[str]: @@ -155,12 +157,12 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: def target_temperature(self) -> None: """Return the temperature corresponding to the PRESET.""" if self.preset_mode == PRESET_ECO: - return self.select_state(CORE_ECO_TARGET_DWH_TEMPERATURE_STATE) + return self.executor.select_state(CORE_ECO_TARGET_DWH_TEMPERATURE_STATE) if self.preset_mode in [PRESET_COMFORT, PRESET_BOOST]: - return self.select_state(CORE_COMFORT_TARGET_DWH_TEMPERATURE_STATE) + return self.executor.select_state(CORE_COMFORT_TARGET_DWH_TEMPERATURE_STATE) - return self.select_state(CORE_TARGET_DWH_TEMPERATURE_STATE) + return self.executor.select_state(CORE_TARGET_DWH_TEMPERATURE_STATE) @property def current_temperature(self): diff --git a/custom_components/tahoma/climate_devices/dimmer_exterior_heating.py b/custom_components/tahoma/climate_devices/dimmer_exterior_heating.py index e07c0e485..ca3114194 100644 --- a/custom_components/tahoma/climate_devices/dimmer_exterior_heating.py +++ b/custom_components/tahoma/climate_devices/dimmer_exterior_heating.py @@ -11,7 +11,7 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from ..coordinator import TahomaDataUpdateCoordinator -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity _LOGGER = logging.getLogger(__name__) @@ -21,13 +21,13 @@ CORE_LEVEL_STATE = "core:LevelState" -class DimmerExteriorHeating(TahomaEntity, ClimateEntity): +class DimmerExteriorHeating(OverkizEntity, ClimateEntity): """Representation of TaHoma IO Atlantic Electrical Heater.""" def __init__(self, device_url: str, coordinator: TahomaDataUpdateCoordinator): """Init method.""" super().__init__(device_url, coordinator) - self._saved_level = 100 - self.select_state(CORE_LEVEL_STATE) + self._saved_level = 100 - self.executor.select_state(CORE_LEVEL_STATE) @property def supported_features(self) -> int: @@ -52,7 +52,7 @@ def max_temp(self) -> float: @property def target_temperature(self): """Return the temperature we try to reach.""" - return 100 - self.select_state(CORE_LEVEL_STATE) + return 100 - self.executor.select_state(CORE_LEVEL_STATE) async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" @@ -65,7 +65,7 @@ async def async_set_temperature(self, **kwargs) -> None: @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" - if self.select_state(CORE_LEVEL_STATE) == 100: + if self.executor.select_state(CORE_LEVEL_STATE) == 100: return HVAC_MODE_OFF return HVAC_MODE_HEAT diff --git a/custom_components/tahoma/climate_devices/evo_home_controller.py b/custom_components/tahoma/climate_devices/evo_home_controller.py index 8464b061e..8a73e3f3e 100644 --- a/custom_components/tahoma/climate_devices/evo_home_controller.py +++ b/custom_components/tahoma/climate_devices/evo_home_controller.py @@ -12,7 +12,7 @@ from homeassistant.const import TEMP_CELSIUS import homeassistant.util.dt as dt_util -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity PRESET_DAY_OFF = "day-off" PRESET_HOLIDAYS = "holidays" @@ -31,7 +31,7 @@ PRESET_MODES_TO_TAHOMA = {v: k for k, v in TAHOMA_TO_PRESET_MODES.items()} -class EvoHomeController(TahomaEntity, ClimateEntity): +class EvoHomeController(OverkizEntity, ClimateEntity): """Representation of EvoHomeController device.""" @property @@ -47,7 +47,7 @@ def supported_features(self) -> int: @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" - operating_mode = self.select_state(RAMSES_RAMSES_OPERATING_MODE_STATE) + operating_mode = self.executor.select_state(RAMSES_RAMSES_OPERATING_MODE_STATE) if operating_mode in TAHOMA_TO_HVAC_MODES: return TAHOMA_TO_HVAC_MODES[operating_mode] @@ -71,7 +71,7 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" - operating_mode = self.select_state(RAMSES_RAMSES_OPERATING_MODE_STATE) + operating_mode = self.executor.select_state(RAMSES_RAMSES_OPERATING_MODE_STATE) if operating_mode in TAHOMA_TO_PRESET_MODES: return TAHOMA_TO_PRESET_MODES[operating_mode] diff --git a/custom_components/tahoma/climate_devices/heating_set_point.py b/custom_components/tahoma/climate_devices/heating_set_point.py index 067e53cad..59cc25909 100644 --- a/custom_components/tahoma/climate_devices/heating_set_point.py +++ b/custom_components/tahoma/climate_devices/heating_set_point.py @@ -10,7 +10,7 @@ TEMP_KELVIN, ) -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity COMMAND_SET_TARGET_TEMPERATURE = "setTargetTemperature" @@ -28,7 +28,7 @@ } -class HeatingSetPoint(TahomaEntity, ClimateEntity): +class HeatingSetPoint(OverkizEntity, ClimateEntity): """Representation of EvoHome HeatingSetPoint device.""" @property @@ -61,7 +61,7 @@ def hvac_modes(self) -> List[str]: @property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" - return self.select_state(CORE_TEMPERATURE_STATE) + return self.executor.select_state(CORE_TEMPERATURE_STATE) @property def target_temperature_step(self) -> Optional[float]: @@ -81,7 +81,7 @@ def max_temp(self) -> float: @property def target_temperature(self): """Return the temperature we try to reach.""" - return self.select_state(CORE_TARGET_TEMPERATURE_STATE) + return self.executor.select_state(CORE_TARGET_TEMPERATURE_STATE) async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" diff --git a/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py b/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py index 6fd66aca6..530e9fcb6 100644 --- a/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py +++ b/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py @@ -26,7 +26,7 @@ ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity _LOGGER = logging.getLogger(__name__) @@ -88,7 +88,7 @@ FAN_MODES_TO_OVP_TAHOMA = {v: k for k, v in OVP_TAHOMA_TO_FAN_MODES.items()} -class HitachiAirToAirHeatPump(TahomaEntity, ClimateEntity): +class HitachiAirToAirHeatPump(OverkizEntity, ClimateEntity): """Representation of HitachiAirToAirHeatPump.""" @property @@ -260,7 +260,7 @@ async def _global_control( def _select_state(self, *states) -> Optional[str]: """Make all strings lowercase, since Hi Kumo server returns capitalized strings for some devices.""" - state = self.select_state(*states) + state = self.executor.select_state(*states) if state and isinstance(state, str): return state.lower() diff --git a/custom_components/tahoma/climate_devices/hitachi_air_to_water_heating_zone.py b/custom_components/tahoma/climate_devices/hitachi_air_to_water_heating_zone.py index 3da728df5..34a7e4bc0 100644 --- a/custom_components/tahoma/climate_devices/hitachi_air_to_water_heating_zone.py +++ b/custom_components/tahoma/climate_devices/hitachi_air_to_water_heating_zone.py @@ -13,7 +13,7 @@ ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity _LOGGER = logging.getLogger(__name__) @@ -54,7 +54,7 @@ PRESET_MODE_TO_TAHOMA = {v: k for k, v in TAHOMA_TO_PRESET_MODE.items()} -class HitachiAirToWaterHeatingZone(TahomaEntity, ClimateEntity): +class HitachiAirToWaterHeatingZone(OverkizEntity, ClimateEntity): """Representation of HitachiAirToWaterHeatingZone.""" @property @@ -79,7 +79,7 @@ def supported_features(self) -> int: def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" return TAHOMA_TO_HVAC_MODE[ - self.select_state(MODBUS_AUTO_MANU_MODE_ZONE_1_STATE) + self.executor.select_state(MODBUS_AUTO_MANU_MODE_ZONE_1_STATE) ] @property @@ -101,7 +101,9 @@ def preset_modes(self) -> Optional[List[str]]: @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" - return TAHOMA_TO_PRESET_MODE[self.select_state(MODBUS_YUTAKI_TARGET_MODE_STATE)] + return TAHOMA_TO_PRESET_MODE[ + self.executor.select_state(MODBUS_YUTAKI_TARGET_MODE_STATE) + ] async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" @@ -112,7 +114,9 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: @property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" - return self.select_state(MODBUS_ROOM_AMBIENT_TEMPERATURE_STATUS_ZONE_1_STATE) + return self.executor.select_state( + MODBUS_ROOM_AMBIENT_TEMPERATURE_STATUS_ZONE_1_STATE + ) @property def min_temp(self) -> float: @@ -132,7 +136,9 @@ def target_temperature_step(self) -> Optional[float]: @property def target_temperature(self): """Return the temperature we try to reach.""" - return self.select_state(MODBUS_THERMOSTAT_SETTING_CONTROL_ZONE_1_STATE) + return self.executor.select_state( + MODBUS_THERMOSTAT_SETTING_CONTROL_ZONE_1_STATE + ) async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" diff --git a/custom_components/tahoma/climate_devices/somfy_thermostat.py b/custom_components/tahoma/climate_devices/somfy_thermostat.py index 86ebb59bd..824322e06 100644 --- a/custom_components/tahoma/climate_devices/somfy_thermostat.py +++ b/custom_components/tahoma/climate_devices/somfy_thermostat.py @@ -25,7 +25,7 @@ from homeassistant.helpers.event import async_track_state_change from ..coordinator import TahomaDataUpdateCoordinator -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity _LOGGER = logging.getLogger(__name__) @@ -74,7 +74,7 @@ } -class SomfyThermostat(TahomaEntity, ClimateEntity): +class SomfyThermostat(OverkizEntity, ClimateEntity): """Representation of Somfy Smart Thermostat.""" def __init__(self, device_url: str, coordinator: TahomaDataUpdateCoordinator): @@ -85,11 +85,11 @@ def __init__(self, device_url: str, coordinator: TahomaDataUpdateCoordinator): if self.preset_mode == PRESET_NONE: self._saved_target_temp = None else: - self._saved_target_temp = self.select_state( + self._saved_target_temp = self.executor.select_state( MAP_PRESET_TEMPERATURES[self.preset_mode] ) else: - self._saved_target_temp = self.select_state( + self._saved_target_temp = self.executor.select_state( CORE_DEROGATED_TARGET_TEMPERATURE_STATE ) self._current_temperature = None @@ -163,7 +163,9 @@ def supported_features(self) -> int: @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" - return MAP_HVAC_MODES[self.select_state(CORE_DEROGATION_ACTIVATION_STATE)] + return MAP_HVAC_MODES[ + self.executor.select_state(CORE_DEROGATION_ACTIVATION_STATE) + ] @property def hvac_modes(self) -> List[str]: @@ -183,8 +185,10 @@ def hvac_action(self) -> str: def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" if self.hvac_mode == HVAC_MODE_AUTO: - return MAP_PRESET_MODES[self.select_state(ST_HEATING_MODE_STATE)] - return MAP_PRESET_MODES[self.select_state(ST_DEROGATION_HEATING_MODE_STATE)] + return MAP_PRESET_MODES[self.executor.select_state(ST_HEATING_MODE_STATE)] + return MAP_PRESET_MODES[ + self.executor.select_state(ST_DEROGATION_HEATING_MODE_STATE) + ] @property def preset_modes(self) -> Optional[List[str]]: @@ -218,8 +222,8 @@ def target_temperature(self): if self.hvac_mode == HVAC_MODE_AUTO: if self.preset_mode == PRESET_NONE: return None - return self.select_state(MAP_PRESET_TEMPERATURES[self.preset_mode]) - return self.select_state(CORE_DEROGATED_TARGET_TEMPERATURE_STATE) + return self.executor.select_state(MAP_PRESET_TEMPERATURES[self.preset_mode]) + return self.executor.select_state(CORE_DEROGATED_TARGET_TEMPERATURE_STATE) async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" diff --git a/custom_components/tahoma/climate_devices/stateless_exterior_heating.py b/custom_components/tahoma/climate_devices/stateless_exterior_heating.py index 5d821a239..ad3e10dee 100644 --- a/custom_components/tahoma/climate_devices/stateless_exterior_heating.py +++ b/custom_components/tahoma/climate_devices/stateless_exterior_heating.py @@ -10,7 +10,7 @@ ) from homeassistant.const import TEMP_CELSIUS -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity _LOGGER = logging.getLogger(__name__) @@ -21,7 +21,7 @@ PRESET_MY = "My" -class StatelessExteriorHeating(TahomaEntity, ClimateEntity): +class StatelessExteriorHeating(OverkizEntity, ClimateEntity): """Representation of TaHoma Stateless Exterior Heating device.""" @property diff --git a/custom_components/tahoma/cover.py b/custom_components/tahoma/cover.py index 50a4f81af..d03bee1bc 100644 --- a/custom_components/tahoma/cover.py +++ b/custom_components/tahoma/cover.py @@ -6,10 +6,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import voluptuous as vol -from custom_components.tahoma.cover_devices.awning import Awning -from custom_components.tahoma.cover_devices.vertical_cover import VerticalCover - from .const import DOMAIN +from .cover_devices.awning import Awning +from .cover_devices.vertical_cover import VerticalCover SERVICE_COVER_MY_POSITION = "set_cover_my_position" SERVICE_COVER_POSITION_LOW_SPEED = "set_cover_position_low_speed" diff --git a/custom_components/tahoma/cover_devices/awning.py b/custom_components/tahoma/cover_devices/awning.py index 4ce35dadd..4dc23c6d2 100644 --- a/custom_components/tahoma/cover_devices/awning.py +++ b/custom_components/tahoma/cover_devices/awning.py @@ -54,7 +54,7 @@ def current_cover_position(self): None is unknown, 0 is closed, 100 is fully open. """ - return self.select_state(CORE_DEPLOYMENT_STATE) + return self.executor.select_state(CORE_DEPLOYMENT_STATE) async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" diff --git a/custom_components/tahoma/cover_devices/tahoma_cover.py b/custom_components/tahoma/cover_devices/tahoma_cover.py index 75e774dfa..431cedb3e 100644 --- a/custom_components/tahoma/cover_devices/tahoma_cover.py +++ b/custom_components/tahoma/cover_devices/tahoma_cover.py @@ -9,7 +9,7 @@ CoverEntity, ) -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity ATTR_OBSTRUCTION_DETECTED = "obstruction-detected" @@ -76,7 +76,7 @@ SUPPORT_COVER_POSITION_LOW_SPEED = 1024 -class TahomaGenericCover(TahomaEntity, CoverEntity): +class TahomaGenericCover(OverkizEntity, CoverEntity): """Representation of a TaHoma Cover.""" @property @@ -85,7 +85,7 @@ def current_cover_tilt_position(self): None is unknown, 0 is closed, 100 is fully open. """ - position = self.select_state( + position = self.executor.select_state( CORE_SLATS_ORIENTATION_STATE, CORE_SLATE_ORIENTATION_STATE ) return 100 - position if position is not None else None @@ -101,7 +101,7 @@ async def async_set_cover_position_low_speed(self, **kwargs): async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" await self.async_execute_command( - self.select_command(*COMMANDS_SET_TILT_POSITION), + self.executor.select_command(*COMMANDS_SET_TILT_POSITION), 100 - kwargs.get(ATTR_TILT_POSITION, 0), ) @@ -109,7 +109,7 @@ async def async_set_cover_tilt_position(self, **kwargs): def is_closed(self): """Return if the cover is closed.""" - state = self.select_state( + state = self.executor.select_state( CORE_OPEN_CLOSED_STATE, CORE_SLATS_OPEN_CLOSED_STATE, CORE_OPEN_CLOSED_PARTIAL_STATE, @@ -133,9 +133,9 @@ 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 + and self.executor.select_state(CORE_PRIORITY_LOCK_TIMER_STATE) > 0 ): - if self.select_state(IO_PRIORITY_LOCK_ORIGINATOR_STATE) == "wind": + if self.executor.select_state(IO_PRIORITY_LOCK_ORIGINATOR_STATE) == "wind": return ICON_WEATHER_WINDY return ICON_LOCK_ALERT @@ -143,11 +143,15 @@ def icon(self): async def async_open_cover_tilt(self, **_): """Open the cover tilt.""" - await self.async_execute_command(self.select_command(*COMMANDS_OPEN_TILT)) + await self.async_execute_command( + self.executor.select_command(*COMMANDS_OPEN_TILT) + ) async def async_close_cover_tilt(self, **_): """Close the cover tilt.""" - await self.async_execute_command(self.select_command(*COMMANDS_CLOSE_TILT)) + await self.async_execute_command( + self.executor.select_command(*COMMANDS_CLOSE_TILT) + ) async def async_stop_cover(self, **_): """Stop the cover.""" @@ -201,7 +205,7 @@ async def async_cancel_or_stop_cover(self, cancel_commands, stop_commands) -> No # Fallback to available stop commands when no executions are found # Stop commands don't work with all devices, due to a bug in Somfy service - await self.async_execute_command(self.select_command(*stop_commands)) + await self.async_execute_command(self.executor.select_command(*stop_commands)) async def async_my(self, **_): """Set cover to preset position.""" diff --git a/custom_components/tahoma/cover_devices/vertical_cover.py b/custom_components/tahoma/cover_devices/vertical_cover.py index b64384c93..5c5b91c9d 100644 --- a/custom_components/tahoma/cover_devices/vertical_cover.py +++ b/custom_components/tahoma/cover_devices/vertical_cover.py @@ -89,7 +89,7 @@ def current_cover_position(self): None is unknown, 0 is closed, 100 is fully open. """ - position = self.select_state( + position = self.executor.select_state( CORE_CLOSURE_STATE, CORE_CLOSURE_OR_ROCKER_POSITION_STATE, CORE_PEDESTRIAN_POSITION_STATE, @@ -108,8 +108,8 @@ async def async_set_cover_position(self, **kwargs): async def async_open_cover(self, **_): """Open the cover.""" - await self.async_execute_command(self.select_command(*COMMANDS_OPEN)) + await self.async_execute_command(self.executor.select_command(*COMMANDS_OPEN)) async def async_close_cover(self, **_): """Close the cover.""" - await self.async_execute_command(self.select_command(*COMMANDS_CLOSE)) + await self.async_execute_command(self.executor.select_command(*COMMANDS_CLOSE)) diff --git a/custom_components/tahoma/entity.py b/custom_components/tahoma/entity.py new file mode 100644 index 000000000..0dd571e86 --- /dev/null +++ b/custom_components/tahoma/entity.py @@ -0,0 +1,138 @@ +"""Parent class for every Overkiz device.""" +import logging +from typing import Any, Dict + +from homeassistant.const import ATTR_BATTERY_LEVEL +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from pyhoma.models import Device + +from .const import DOMAIN +from .coordinator import TahomaDataUpdateCoordinator +from .executor import OverkizExecutor + +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" + +STATE_AVAILABLE = "available" +STATE_BATTERY_FULL = "full" +STATE_BATTERY_NORMAL = "normal" +STATE_BATTERY_LOW = "low" +STATE_BATTERY_VERY_LOW = "verylow" +STATE_DEAD = "dead" + +BATTERY_MAP = { + STATE_BATTERY_FULL: 100, + STATE_BATTERY_NORMAL: 75, + STATE_BATTERY_LOW: 25, + STATE_BATTERY_VERY_LOW: 10, +} + +_LOGGER = logging.getLogger(__name__) + + +class OverkizEntity(CoordinatorEntity, Entity): + """Representation of a Overkiz device entity.""" + + def __init__(self, device_url: str, coordinator: TahomaDataUpdateCoordinator): + """Initialize the device.""" + super().__init__(coordinator) + self.device_url = device_url + self.executor = OverkizExecutor(device_url, coordinator) + + @property + def device(self) -> Device: + """Return Overkiz device linked to this entity.""" + return self.coordinator.data[self.device_url] + + @property + def name(self) -> str: + """Return the name of the device.""" + return self.device.label + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.device.available + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self.device.deviceurl + + @property + def assumed_state(self) -> bool: + """Return True if unable to access real state of the entity.""" + return not self.device.states + + @property + def device_info(self) -> Dict[str, Any]: + """Return device registry information for this entity.""" + # Some devices, such as the Smart Thermostat have several devices in one physical device, + # with same device url, terminated by '#' and a number. + # In this case, we use the base device url as the device identifier. + if "#" in self.device_url and not self.device_url.endswith("#1"): + # Only return the url of the base device, to inherit device name and model from parent device. + return { + "identifiers": {(DOMAIN, self.executor.base_device_url)}, + } + + manufacturer = ( + self.executor.select_attribute(CORE_MANUFACTURER) + or self.executor.select_state(CORE_MANUFACTURER_NAME_STATE) + or "Somfy" + ) + + model = ( + self.executor.select_state( + CORE_MODEL_STATE, CORE_PRODUCT_MODEL_NAME_STATE, IO_MODEL_STATE + ) + or self.device.widget + ) + + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "manufacturer": manufacturer, + "name": self.name, + "model": model, + "sw_version": self.device.controllable_name, + "suggested_area": self.coordinator.areas[self.device.placeoid], + "via_device": self.executor.get_gateway_id(), + } + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return the state attributes of the device.""" + attr = {} + + if self.executor.has_state(CORE_RSSI_LEVEL_STATE): + attr[ATTR_RSSI_LEVEL] = self.executor.select_state(CORE_RSSI_LEVEL_STATE) + + if self.executor.has_state(CORE_BATTERY_STATE): + battery_state = self.executor.select_state(CORE_BATTERY_STATE) + attr[ATTR_BATTERY_LEVEL] = BATTERY_MAP.get(battery_state, battery_state) + + if self.executor.select_state(CORE_SENSOR_DEFECT_STATE) == STATE_DEAD: + attr[ATTR_BATTERY_LEVEL] = 0 + + if self.device.attributes: + for attribute in self.device.attributes: + attr[attribute.name] = attribute.value + + if self.device.states: + for state in self.device.states: + if "State" in state.name: + attr[state.name] = state.value + + return attr diff --git a/custom_components/tahoma/executor.py b/custom_components/tahoma/executor.py new file mode 100644 index 000000000..4a3a8be9f --- /dev/null +++ b/custom_components/tahoma/executor.py @@ -0,0 +1,104 @@ +"""Class for helpers and community with the OverKiz API.""" +import logging +from typing import Any, Optional +from urllib.parse import urlparse + +from pyhoma.models import Command, Device + +from .coordinator import TahomaDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +class OverkizExecutor: + """Representation of an Overkiz device with execution handler.""" + + def __init__(self, device_url: str, coordinator: TahomaDataUpdateCoordinator): + """Initialize the executor.""" + self.device_url = device_url + self.coordinator = coordinator + self.base_device_url = self.get_base_device_url() + + @property + def device(self) -> Device: + """Return Overkiz device linked to this entity.""" + return self.coordinator.data[self.device_url] + + def select_command(self, *commands: str) -> Optional[str]: + """Select first existing command in a list of commands.""" + existing_commands = self.device.definition.commands + return next((c for c in commands if c in existing_commands), None) + + def has_command(self, *commands: str) -> bool: + """Return True if a command exists in a list of commands.""" + return self.select_command(*commands) is not None + + def select_state(self, *states) -> Optional[str]: + """Select first existing active state in a list of states.""" + if self.device.states: + return next( + ( + state.value + for state in self.device.states + if state.name in list(states) + ), + None, + ) + return None + + def has_state(self, *states: str) -> bool: + """Return True if a state exists in self.""" + return self.select_state(*states) is not None + + def select_attribute(self, *attributes) -> Optional[str]: + """Select first existing active state in a list of states.""" + if self.device.attributes: + return next( + ( + attribute.value + for attribute in self.device.attributes + if attribute.name in list(attributes) + ), + None, + ) + + async def async_execute_command(self, command_name: str, *args: Any): + """Execute device command in async context.""" + try: + exec_id = await self.coordinator.client.execute_command( + self.device.deviceurl, + Command(command_name, list(args)), + "Home Assistant", + ) + except Exception as exception: # pylint: disable=broad-except + _LOGGER.error(exception) + return + + # ExecutionRegisteredEvent doesn't contain the deviceurl, thus we need to register it here + self.coordinator.executions[exec_id] = { + "deviceurl": self.device.deviceurl, + "command_name": command_name, + } + + await self.coordinator.async_refresh() + + 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_gateway_id(self): + """ + Retrieve gateway id from device url. + + device URL (:///[#]) + """ + url = urlparse(self.device_url) + return url.netloc + + 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 diff --git a/custom_components/tahoma/light.py b/custom_components/tahoma/light.py index 062e51ec8..e5b771d2f 100644 --- a/custom_components/tahoma/light.py +++ b/custom_components/tahoma/light.py @@ -20,7 +20,7 @@ from .const import COMMAND_OFF, COMMAND_ON, CORE_ON_OFF_STATE, DOMAIN from .coordinator import TahomaDataUpdateCoordinator -from .tahoma_entity import TahomaEntity +from .entity import OverkizEntity _LOGGER = logging.getLogger(__name__) @@ -61,7 +61,7 @@ async def async_setup_entry( ) -class TahomaLight(TahomaEntity, LightEntity): +class TahomaLight(OverkizEntity, LightEntity): """Representation of a TaHoma Light.""" def __init__(self, device_url: str, coordinator: TahomaDataUpdateCoordinator): @@ -72,20 +72,20 @@ def __init__(self, device_url: str, coordinator: TahomaDataUpdateCoordinator): @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - brightness = self.select_state(CORE_LIGHT_INTENSITY_STATE) + brightness = self.executor.select_state(CORE_LIGHT_INTENSITY_STATE) return round(brightness * 255 / 100) @property def is_on(self) -> bool: """Return true if light is on.""" - return self.select_state(CORE_ON_OFF_STATE) == STATE_ON + return self.executor.select_state(CORE_ON_OFF_STATE) == STATE_ON @property def hs_color(self): """Return the hue and saturation color value [float, float].""" - r = self.select_state(CORE_RED_COLOR_INTENSITY_STATE) - g = self.select_state(CORE_GREEN_COLOR_INTENSITY_STATE) - b = self.select_state(CORE_BLUE_COLOR_INTENSITY_STATE) + r = self.executor.select_state(CORE_RED_COLOR_INTENSITY_STATE) + g = self.executor.select_state(CORE_GREEN_COLOR_INTENSITY_STATE) + b = self.executor.select_state(CORE_BLUE_COLOR_INTENSITY_STATE) return None if None in [r, g, b] else color_util.color_RGB_to_hs(r, g, b) @property @@ -93,16 +93,16 @@ def supported_features(self) -> int: """Flag supported features.""" supported_features = 0 - if self.has_command(COMMAND_SET_INTENSITY): + if self.executor.has_command(COMMAND_SET_INTENSITY): supported_features |= SUPPORT_BRIGHTNESS - if self.has_command(COMMAND_WINK): + if self.executor.has_command(COMMAND_WINK): supported_features |= SUPPORT_EFFECT - if self.has_command(COMMAND_SET_RGB): + if self.executor.has_command(COMMAND_SET_RGB): supported_features |= SUPPORT_COLOR - if self.has_command(COMMAND_MY): + if self.executor.has_command(COMMAND_MY): supported_features |= SUPPORT_MY return supported_features @@ -110,7 +110,7 @@ def supported_features(self) -> int: async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" if ATTR_HS_COLOR in kwargs: - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_RGB, *[ round(float(c)) @@ -120,27 +120,27 @@ async def async_turn_on(self, **kwargs) -> None: if ATTR_BRIGHTNESS in kwargs: brightness = round(float(kwargs[ATTR_BRIGHTNESS]) / 255 * 100) - await self.async_execute_command(COMMAND_SET_INTENSITY, brightness) + await self.executor.async_execute_command(COMMAND_SET_INTENSITY, brightness) elif ATTR_EFFECT in kwargs: self._effect = kwargs[ATTR_EFFECT] - await self.async_execute_command(self._effect, 100) + await self.executor.async_execute_command(self._effect, 100) else: - await self.async_execute_command(COMMAND_ON) + await self.executor.async_execute_command(COMMAND_ON) async def async_turn_off(self, **_) -> None: """Turn the light off.""" - await self.async_execute_command(COMMAND_OFF) + await self.executor.async_execute_command(COMMAND_OFF) async def async_my(self, **_): """Set light to preset position.""" - await self.async_execute_command(COMMAND_MY) + await self.executor.async_execute_command(COMMAND_MY) @property def effect_list(self) -> list: """Return the list of supported effects.""" - return [COMMAND_WINK] if self.has_command(COMMAND_WINK) else None + return [COMMAND_WINK] if self.executor.has_command(COMMAND_WINK) else None @property def effect(self) -> str: diff --git a/custom_components/tahoma/lock.py b/custom_components/tahoma/lock.py index b9ebc3921..a7ff011d3 100644 --- a/custom_components/tahoma/lock.py +++ b/custom_components/tahoma/lock.py @@ -8,7 +8,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .tahoma_entity import TahomaEntity +from .entity import OverkizEntity _LOGGER = logging.getLogger(__name__) @@ -34,18 +34,18 @@ async def async_setup_entry( async_add_entities(entities) -class TahomaLock(TahomaEntity, LockEntity): +class TahomaLock(OverkizEntity, LockEntity): """Representation of a TaHoma Lock.""" async def async_unlock(self, **_): """Unlock method.""" - await self.async_execute_command(COMMAND_UNLOCK) + await self.executor.async_execute_command(COMMAND_UNLOCK) async def async_lock(self, **_): """Lock method.""" - await self.async_execute_command(COMMAND_LOCK) + await self.executor.async_execute_command(COMMAND_LOCK) @property def is_locked(self): """Return True if the lock is locked.""" - return self.select_state(CORE_LOCKED_UNLOCKED_STATE) == STATE_LOCKED + return self.executor.select_state(CORE_LOCKED_UNLOCKED_STATE) == STATE_LOCKED diff --git a/custom_components/tahoma/sensor.py b/custom_components/tahoma/sensor.py index e69cbcfbb..df27caf23 100644 --- a/custom_components/tahoma/sensor.py +++ b/custom_components/tahoma/sensor.py @@ -40,7 +40,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .tahoma_entity import TahomaEntity +from .tahoma_entity import OverkizEntity _LOGGER = logging.getLogger(__name__) @@ -131,13 +131,13 @@ async def async_setup_entry( async_add_entities(entities) -class TahomaSensor(TahomaEntity, Entity): +class TahomaSensor(OverkizEntity, Entity): """Representation of a TaHoma Sensor.""" @property def state(self): """Return the value of the sensor.""" - state = self.select_state( + state = self.executor.select_state( CORE_CO2_CONCENTRATION_STATE, CORE_CO_CONCENTRATION_STATE, CORE_ELECTRIC_ENERGY_CONSUMPTION_STATE, diff --git a/custom_components/tahoma/switch.py b/custom_components/tahoma/switch.py index 832174646..92ca12f8f 100644 --- a/custom_components/tahoma/switch.py +++ b/custom_components/tahoma/switch.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import COMMAND_OFF, COMMAND_ON, CORE_ON_OFF_STATE, DOMAIN -from .tahoma_entity import TahomaEntity +from .entity import OverkizEntity _LOGGER = logging.getLogger(__name__) @@ -48,7 +48,7 @@ async def async_setup_entry( async_add_entities(entities) -class TahomaSwitch(TahomaEntity, SwitchEntity): +class TahomaSwitch(OverkizEntity, SwitchEntity): """Representation a TaHoma Switch.""" @property @@ -111,4 +111,7 @@ async def async_toggle(self, **_): @property def is_on(self): """Get whether the switch is in on state.""" - return self.select_state(CORE_ON_OFF_STATE, IO_FORCE_HEATING_STATE) == STATE_ON + return ( + self.executor.select_state(CORE_ON_OFF_STATE, IO_FORCE_HEATING_STATE) + == STATE_ON + ) diff --git a/custom_components/tahoma/tahoma_entity.py b/custom_components/tahoma/tahoma_entity.py deleted file mode 100644 index d5458964d..000000000 --- a/custom_components/tahoma/tahoma_entity.py +++ /dev/null @@ -1,217 +0,0 @@ -"""Parent class for every TaHoma device.""" -import logging -import re -from typing import Any, Dict, Optional - -from homeassistant.const import ATTR_BATTERY_LEVEL -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from pyhoma.models import Command, Device - -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" - -STATE_AVAILABLE = "available" -STATE_BATTERY_FULL = "full" -STATE_BATTERY_NORMAL = "normal" -STATE_BATTERY_LOW = "low" -STATE_BATTERY_VERY_LOW = "verylow" -STATE_DEAD = "dead" - -BATTERY_MAP = { - STATE_BATTERY_FULL: 100, - STATE_BATTERY_NORMAL: 75, - STATE_BATTERY_LOW: 25, - STATE_BATTERY_VERY_LOW: 10, -} - -_LOGGER = logging.getLogger(__name__) - - -class TahomaEntity(CoordinatorEntity, Entity): - """Representation of a TaHoma device entity.""" - - 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() - - @property - def device(self) -> Device: - """Return TaHoma device linked to this entity.""" - return self.coordinator.data[self.device_url] - - @property - def name(self) -> str: - """Return the name of the device.""" - return self.device.label - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self.device.available - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self.device.deviceurl - - @property - def assumed_state(self) -> bool: - """Return True if unable to access real state of the entity.""" - return not self.device.states - - @property - 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) - - if self.select_state(CORE_SENSOR_DEFECT_STATE) == STATE_DEAD: - attr[ATTR_BATTERY_LEVEL] = 0 - - if self.device.attributes: - for attribute in self.device.attributes: - attr[attribute.name] = attribute.value - - if self.device.states: - for state in self.device.states: - if "State" in state.name: - attr[state.name] = state.value - - return attr - - @property - def device_info(self) -> Dict[str, Any]: - """Return device registry information for this entity.""" - # Some devices, such as the Smart Thermostat have several devices in one physical device, - # with same device url, terminated by '#' and a number. - # In this case, we use the base device url as the device identifier. - if "#" in self.device_url and not self.device_url.endswith("#1"): - # Only return the url of the base device, to inherit device name and model from parent device. - return { - "identifiers": {(DOMAIN, self.base_device_url)}, - } - - manufacturer = ( - self.select_attribute(CORE_MANUFACTURER) - or self.select_state(CORE_MANUFACTURER_NAME_STATE) - or "Somfy" - ) - model = ( - self.select_state( - CORE_MODEL_STATE, CORE_PRODUCT_MODEL_NAME_STATE, IO_MODEL_STATE - ) - or self.device.widget - ) - - return { - "identifiers": {(DOMAIN, self.base_device_url)}, - "name": self.name, - "manufacturer": manufacturer, - "model": model, - "sw_version": self.device.controllable_name, - "via_device": self.get_gateway_id(), - "suggested_area": self.coordinator.areas[self.device.placeoid], - } - - def select_command(self, *commands: str) -> Optional[str]: - """Select first existing command in a list of commands.""" - existing_commands = self.device.definition.commands - - return next((c for c in commands if c in existing_commands), None) - - def has_command(self, *commands: str) -> bool: - """Return True if a command exists in a list of commands.""" - return self.select_command(*commands) is not None - - def select_state(self, *states) -> Optional[str]: - """Select first existing active state in a list of states.""" - if self.device.states: - return next( - ( - state.value - for state in self.device.states - if state.name in list(states) - ), - None, - ) - return None - - def has_state(self, *states: str) -> bool: - """Return True if a state exists in self.""" - return self.select_state(*states) is not None - - def select_attribute(self, *attributes) -> Optional[str]: - """Select first existing active state in a list of states.""" - if self.device.attributes: - return next( - ( - attribute.value - for attribute in self.device.attributes - if attribute.name in list(attributes) - ), - None, - ) - - async def async_execute_command(self, command_name: str, *args: Any): - """Execute device command in async context.""" - try: - exec_id = await self.coordinator.client.execute_command( - self.device.deviceurl, - Command(command_name, list(args)), - "Home Assistant", - ) - except Exception as exception: # pylint: disable=broad-except - _LOGGER.error(exception) - return - - # ExecutionRegisteredEvent doesn't contain the deviceurl, thus we need to register it here - self.coordinator.executions[exec_id] = { - "deviceurl": self.device.deviceurl, - "command_name": command_name, - } - - await self.coordinator.async_refresh() - - 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) - - if result: - return result.group(1) - else: - return None diff --git a/custom_components/tahoma/water_heater_devices/domestic_hot_water_production.py b/custom_components/tahoma/water_heater_devices/domestic_hot_water_production.py index 324cda130..9e6ae3a47 100644 --- a/custom_components/tahoma/water_heater_devices/domestic_hot_water_production.py +++ b/custom_components/tahoma/water_heater_devices/domestic_hot_water_production.py @@ -8,7 +8,7 @@ ) from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON, TEMP_CELSIUS -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE_STATE = "core:MaximalTemperatureManualModeState" CORE_MINIMAL_TEMPERATURE_MANUAL_MODE_STATE = "core:MinimalTemperatureManualModeState" @@ -40,7 +40,7 @@ MAP_REVERSE_OPERATION_MODES = {v: k for k, v in MAP_OPERATION_MODES.items()} -class DomesticHotWaterProduction(TahomaEntity, WaterHeaterEntity): +class DomesticHotWaterProduction(OverkizEntity, WaterHeaterEntity): """Representation of a DomesticHotWaterProduction Water Heater.""" @property @@ -51,17 +51,17 @@ def temperature_unit(self) -> str: @property def min_temp(self): """Return the minimum temperature.""" - return self.select_state(CORE_MINIMAL_TEMPERATURE_MANUAL_MODE_STATE) + return self.executor.select_state(CORE_MINIMAL_TEMPERATURE_MANUAL_MODE_STATE) @property def max_temp(self): """Return the maximum temperature.""" - return self.select_state(CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE_STATE) + return self.executor.select_state(CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE_STATE) @property def current_operation(self): """Return current operation ie. eco, electric, performance, ...""" - return MAP_OPERATION_MODES[self.select_state(IO_DHW_MODE_STATE)] + return MAP_OPERATION_MODES[self.executor.select_state(IO_DHW_MODE_STATE)] @property def operation_list(self): @@ -71,22 +71,22 @@ def operation_list(self): @property def current_temperature(self): """Return the current temperature.""" - return self.select_state(IO_MIDDLE_WATER_TEMPERATURE_STATE) + return self.executor.select_state(IO_MIDDLE_WATER_TEMPERATURE_STATE) @property def target_temperature(self): """Return the temperature we try to reach.""" - return self.select_state(CORE_TARGET_TEMPERATURE_STATE) + return self.executor.select_state(CORE_TARGET_TEMPERATURE_STATE) @property def target_temperature_high(self): """Return the highbound target temperature we try to reach.""" - return self.select_state(CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE_STATE) + return self.executor.select_state(CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE_STATE) @property def target_temperature_low(self): """Return the lowbound target temperature we try to reach.""" - return self.select_state(CORE_MINIMAL_TEMPERATURE_MANUAL_MODE_STATE) + return self.executor.select_state(CORE_MINIMAL_TEMPERATURE_MANUAL_MODE_STATE) async def async_set_temperature(self, **kwargs): """Set new target temperature.""" @@ -110,7 +110,8 @@ def supported_features(self): def is_away_mode_on(self): """Return true if away mode is on.""" return ( - self.select_state(CORE_OPERATING_MODE_STATE).get(STATE_ABSENCE) == STATE_ON + self.executor.select_state(CORE_OPERATING_MODE_STATE).get(STATE_ABSENCE) + == STATE_ON ) async def async_turn_away_mode_on(self): diff --git a/custom_components/tahoma/water_heater_devices/hitachi_dhw.py b/custom_components/tahoma/water_heater_devices/hitachi_dhw.py index 118139b4c..97c1a2fbc 100644 --- a/custom_components/tahoma/water_heater_devices/hitachi_dhw.py +++ b/custom_components/tahoma/water_heater_devices/hitachi_dhw.py @@ -12,7 +12,7 @@ TEMP_CELSIUS, ) -from ..tahoma_entity import TahomaEntity +from ..entity import OverkizEntity CORE_DHW_TEMPERATURE_STATE = "core:DHWTemperatureState" MODBUS_DHW_MODE_STATE = "modbus:DHWModeState" @@ -41,7 +41,7 @@ OPERATION_MODE_TO_TAHOMA = {v: k for k, v in TAHOMA_TO_OPERATION_MODE.items()} -class HitachiDHW(TahomaEntity, WaterHeaterEntity): +class HitachiDHW(OverkizEntity, WaterHeaterEntity): """Representation of a HitachiDHW Water Heater.""" @property @@ -72,12 +72,12 @@ def precision(self): @property def current_temperature(self): """Return the current temperature.""" - return self.select_state(CORE_DHW_TEMPERATURE_STATE) + return self.executor.select_state(CORE_DHW_TEMPERATURE_STATE) @property def target_temperature(self): """Return the temperature we try to reach.""" - return self.select_state(MODBUS_CONTROL_DHW_SETTING_TEMPERATURE_STATE) + return self.executor.select_state(MODBUS_CONTROL_DHW_SETTING_TEMPERATURE_STATE) async def async_set_temperature(self, **kwargs): """Set new target temperature.""" @@ -89,10 +89,12 @@ async def async_set_temperature(self, **kwargs): @property def current_operation(self): """Return current operation ie. eco, electric, performance, ...""" - if self.select_state(MODBUS_CONTROL_DHW_STATE) == STATE_STOP: + if self.executor.select_state(MODBUS_CONTROL_DHW_STATE) == STATE_STOP: return STATE_OFF - return TAHOMA_TO_OPERATION_MODE[self.select_state(MODBUS_DHW_MODE_STATE)] + return TAHOMA_TO_OPERATION_MODE[ + self.executor.select_state(MODBUS_DHW_MODE_STATE) + ] @property def operation_list(self): From ae004c545a66ff7e57a561a3ee0bdc8120c88b50 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Wed, 4 Aug 2021 21:09:41 +0200 Subject: [PATCH 2/6] Change missing references --- .../tahoma/alarm_control_panel.py | 22 ++++++++++--------- ...er_with_adjustable_temperature_setpoint.py | 4 ++-- .../hitachi_air_to_air_heat_pump.py | 2 +- .../tahoma/cover_devices/awning.py | 8 +++---- .../tahoma/cover_devices/tahoma_cover.py | 16 +++++++------- .../tahoma/cover_devices/vertical_cover.py | 8 +++---- custom_components/tahoma/switch.py | 14 ++++++------ 7 files changed, 38 insertions(+), 36 deletions(-) diff --git a/custom_components/tahoma/alarm_control_panel.py b/custom_components/tahoma/alarm_control_panel.py index fbbe0bdc1..f8201f02a 100644 --- a/custom_components/tahoma/alarm_control_panel.py +++ b/custom_components/tahoma/alarm_control_panel.py @@ -101,7 +101,9 @@ class TahomaAlarmControlPanel(OverkizEntity, AlarmControlPanelEntity): @property def state(self): """Return the state of the device.""" - if self.has_state(CORE_INTRUSION_STATE, INTERNAL_INTRUSION_DETECTED_STATE): + if self.executor.has_state( + CORE_INTRUSION_STATE, INTERNAL_INTRUSION_DETECTED_STATE + ): state = self.executor.select_state( CORE_INTRUSION_STATE, INTERNAL_INTRUSION_DETECTED_STATE ) @@ -111,24 +113,24 @@ def state(self): return STATE_ALARM_PENDING if ( - self.has_state(INTERNAL_CURRENT_ALARM_MODE_STATE) - and self.has_state(INTERNAL_TARGET_ALARM_MODE_STATE) + self.executor.has_state(INTERNAL_CURRENT_ALARM_MODE_STATE) + and self.executor.has_state(INTERNAL_TARGET_ALARM_MODE_STATE) and self.executor.select_state(INTERNAL_CURRENT_ALARM_MODE_STATE) != self.executor.select_state(INTERNAL_TARGET_ALARM_MODE_STATE) ): return STATE_ALARM_PENDING - if self.has_state(MYFOX_ALARM_STATUS_STATE): + if self.executor.has_state(MYFOX_ALARM_STATUS_STATE): return MAP_MYFOX_STATUS_STATE[ self.executor.select_state(MYFOX_ALARM_STATUS_STATE) ] - if self.has_state(INTERNAL_CURRENT_ALARM_MODE_STATE): + if self.executor.has_state(INTERNAL_CURRENT_ALARM_MODE_STATE): return MAP_INTERNAL_STATUS_STATE[ self.executor.select_state(INTERNAL_CURRENT_ALARM_MODE_STATE) ] - if self.has_state(VERISURE_ALARM_PANEL_MAIN_ARM_TYPE_STATE): + if self.executor.has_state(VERISURE_ALARM_PANEL_MAIN_ARM_TYPE_STATE): return MAP_VERISURE_STATUS_STATE[ self.executor.select_state(VERISURE_ALARM_PANEL_MAIN_ARM_TYPE_STATE) ] @@ -140,18 +142,18 @@ def supported_features(self) -> int: """Return the list of supported features.""" supported_features = 0 - if self.has_command(COMMAND_ARM, COMMAND_ALARM_ON): + if self.executor.has_command(COMMAND_ARM, COMMAND_ALARM_ON): supported_features |= SUPPORT_ALARM_ARM_AWAY - if self.has_command(COMMAND_ALARM_PARTIAL_1, COMMAND_ARM_PARTIAL_DAY): + if self.executor.has_command(COMMAND_ALARM_PARTIAL_1, COMMAND_ARM_PARTIAL_DAY): supported_features |= SUPPORT_ALARM_ARM_HOME - if self.has_command( + if self.executor.has_command( COMMAND_PARTIAL, COMMAND_ALARM_PARTIAL_2, COMMAND_ARM_PARTIAL_NIGHT ): supported_features |= SUPPORT_ALARM_ARM_NIGHT - if self.has_command(COMMAND_SET_ALARM_STATUS): + if self.executor.has_command(COMMAND_SET_ALARM_STATUS): supported_features |= SUPPORT_ALARM_TRIGGER supported_features |= SUPPORT_ALARM_ARM_CUSTOM_BYPASS diff --git a/custom_components/tahoma/climate_devices/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py b/custom_components/tahoma/climate_devices/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py index f53da3c9d..213d9522b 100644 --- a/custom_components/tahoma/climate_devices/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py +++ b/custom_components/tahoma/climate_devices/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py @@ -164,10 +164,10 @@ def supported_features(self) -> int: """Return the list of supported features.""" supported_features = 0 - if self.has_command(COMMAND_SET_HEATING_LEVEL): + if self.executor.has_command(COMMAND_SET_HEATING_LEVEL): supported_features |= SUPPORT_PRESET_MODE - if self.has_command(COMMAND_SET_TARGET_TEMPERATURE): + if self.executor.has_command(COMMAND_SET_TARGET_TEMPERATURE): supported_features |= SUPPORT_TARGET_TEMPERATURE return supported_features diff --git a/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py b/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py index 530e9fcb6..ba734d1ce 100644 --- a/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py +++ b/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py @@ -104,7 +104,7 @@ def supported_features(self) -> int: SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_PRESET_MODE ) - if self.has_state(*SWING_STATE): + if self.executor.has_state(*SWING_STATE): supported_features |= SUPPORT_SWING_MODE return supported_features diff --git a/custom_components/tahoma/cover_devices/awning.py b/custom_components/tahoma/cover_devices/awning.py index 4dc23c6d2..7fdcc45b3 100644 --- a/custom_components/tahoma/cover_devices/awning.py +++ b/custom_components/tahoma/cover_devices/awning.py @@ -28,16 +28,16 @@ def supported_features(self): """Flag supported features.""" supported_features = super().supported_features - if self.has_command(COMMAND_SET_DEPLOYMENT): + if self.executor.has_command(COMMAND_SET_DEPLOYMENT): supported_features |= SUPPORT_SET_POSITION - if self.has_command(COMMAND_DEPLOY): + if self.executor.has_command(COMMAND_DEPLOY): supported_features |= SUPPORT_OPEN - if self.has_command(*COMMANDS_STOP): + if self.executor.has_command(*COMMANDS_STOP): supported_features |= SUPPORT_STOP - if self.has_command(COMMAND_UNDEPLOY): + if self.executor.has_command(COMMAND_UNDEPLOY): supported_features |= SUPPORT_CLOSE return supported_features diff --git a/custom_components/tahoma/cover_devices/tahoma_cover.py b/custom_components/tahoma/cover_devices/tahoma_cover.py index 431cedb3e..1217c6288 100644 --- a/custom_components/tahoma/cover_devices/tahoma_cover.py +++ b/custom_components/tahoma/cover_devices/tahoma_cover.py @@ -132,7 +132,7 @@ def is_closed(self): def icon(self): """Return the icon to use in the frontend, if any.""" if ( - self.has_state(CORE_PRIORITY_LOCK_TIMER_STATE) + self.executor.has_state(CORE_PRIORITY_LOCK_TIMER_STATE) and self.executor.select_state(CORE_PRIORITY_LOCK_TIMER_STATE) > 0 ): if self.executor.select_state(IO_PRIORITY_LOCK_ORIGINATOR_STATE) == "wind": @@ -267,7 +267,7 @@ def device_state_attributes(self): attr = super().device_state_attributes or {} # Obstruction Detected attribute is used by HomeKit - if self.has_state(IO_PRIORITY_LOCK_LEVEL_STATE): + if self.executor.has_state(IO_PRIORITY_LOCK_LEVEL_STATE): attr[ATTR_OBSTRUCTION_DETECTED] = True return attr @@ -277,22 +277,22 @@ def supported_features(self): """Flag supported features.""" supported_features = 0 - if self.has_command(*COMMANDS_OPEN_TILT): + if self.executor.has_command(*COMMANDS_OPEN_TILT): supported_features |= SUPPORT_OPEN_TILT - if self.has_command(*COMMANDS_STOP_TILT): + if self.executor.has_command(*COMMANDS_STOP_TILT): supported_features |= SUPPORT_STOP_TILT - if self.has_command(*COMMANDS_CLOSE_TILT): + if self.executor.has_command(*COMMANDS_CLOSE_TILT): supported_features |= SUPPORT_CLOSE_TILT - if self.has_command(*COMMANDS_SET_TILT_POSITION): + if self.executor.has_command(*COMMANDS_SET_TILT_POSITION): supported_features |= SUPPORT_SET_TILT_POSITION - if self.has_command(COMMAND_SET_CLOSURE_AND_LINEAR_SPEED): + if self.executor.has_command(COMMAND_SET_CLOSURE_AND_LINEAR_SPEED): supported_features |= SUPPORT_COVER_POSITION_LOW_SPEED - if self.has_command(COMMAND_MY): + if self.executor.has_command(COMMAND_MY): supported_features |= SUPPORT_MY return supported_features diff --git a/custom_components/tahoma/cover_devices/vertical_cover.py b/custom_components/tahoma/cover_devices/vertical_cover.py index 5c5b91c9d..58e1a46de 100644 --- a/custom_components/tahoma/cover_devices/vertical_cover.py +++ b/custom_components/tahoma/cover_devices/vertical_cover.py @@ -59,16 +59,16 @@ def supported_features(self): """Flag supported features.""" supported_features = super().supported_features - if self.has_command(COMMAND_SET_CLOSURE): + if self.executor.has_command(COMMAND_SET_CLOSURE): supported_features |= SUPPORT_SET_POSITION - if self.has_command(*COMMANDS_OPEN): + if self.executor.has_command(*COMMANDS_OPEN): supported_features |= SUPPORT_OPEN - if self.has_command(*COMMANDS_STOP): + if self.executor.has_command(*COMMANDS_STOP): supported_features |= SUPPORT_STOP - if self.has_command(*COMMANDS_CLOSE): + if self.executor.has_command(*COMMANDS_CLOSE): supported_features |= SUPPORT_CLOSE return supported_features diff --git a/custom_components/tahoma/switch.py b/custom_components/tahoma/switch.py index 92ca12f8f..f4a9af464 100644 --- a/custom_components/tahoma/switch.py +++ b/custom_components/tahoma/switch.py @@ -71,13 +71,13 @@ def icon(self) -> Optional[str]: async def async_turn_on(self, **_): """Send the on command.""" - if self.has_command(COMMAND_ON): + if self.executor.has_command(COMMAND_ON): await self.async_execute_command(COMMAND_ON) - elif self.has_command(COMMAND_SET_FORCE_HEATING): + elif self.executor.has_command(COMMAND_SET_FORCE_HEATING): await self.async_execute_command(COMMAND_SET_FORCE_HEATING, STATE_ON) - elif self.has_command(COMMAND_RING_WITH_SINGLE_SIMPLE_SEQUENCE): + elif self.executor.has_command(COMMAND_RING_WITH_SINGLE_SIMPLE_SEQUENCE): await self.async_execute_command( COMMAND_RING_WITH_SINGLE_SIMPLE_SEQUENCE, # https://www.tahomalink.com/enduser-mobile-web/steer-html5-client/vendor/somfy/io/siren/const.js 2 * 60 * 1000, # 2 minutes @@ -88,7 +88,7 @@ async def async_turn_on(self, **_): async def async_turn_off(self, **_): """Send the off command.""" - if self.has_command(COMMAND_RING_WITH_SINGLE_SIMPLE_SEQUENCE): + if self.executor.has_command(COMMAND_RING_WITH_SINGLE_SIMPLE_SEQUENCE): await self.async_execute_command( COMMAND_RING_WITH_SINGLE_SIMPLE_SEQUENCE, 2000, @@ -97,15 +97,15 @@ async def async_turn_off(self, **_): COMMAND_STANDARD, ) - elif self.has_command(COMMAND_SET_FORCE_HEATING): + elif self.executor.has_command(COMMAND_SET_FORCE_HEATING): await self.async_execute_command(COMMAND_SET_FORCE_HEATING, STATE_OFF) - elif self.has_command(COMMAND_OFF): + elif self.executor.has_command(COMMAND_OFF): await self.async_execute_command(COMMAND_OFF) async def async_toggle(self, **_): """Click the switch.""" - if self.has_command(COMMAND_CYCLE): + if self.executor.has_command(COMMAND_CYCLE): await self.async_execute_command(COMMAND_CYCLE) @property From ead912c1c27d59d20dd06857506e8b8d3e7b75e3 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Wed, 4 Aug 2021 21:10:50 +0200 Subject: [PATCH 3/6] Fix execute command --- .../tahoma/alarm_control_panel.py | 12 ++++++------ .../atlantic_electrical_heater.py | 4 ++-- ...ter_with_adjustable_temperature_setpoint.py | 14 ++++++++------ .../atlantic_electrical_towel_dryer.py | 8 ++++---- ...lantic_pass_apc_heating_and_cooling_zone.py | 18 ++++++++++++------ .../atlantic_pass_apc_zone_control.py | 2 +- .../climate_devices/atlantic_pass_apcdhw.py | 18 +++++++++++------- .../climate_devices/dimmer_exterior_heating.py | 8 ++++---- .../climate_devices/evo_home_controller.py | 4 ++-- .../climate_devices/heating_set_point.py | 2 +- .../hitachi_air_to_air_heat_pump.py | 4 ++-- .../hitachi_air_to_water_heating_zone.py | 6 +++--- .../tahoma/climate_devices/somfy_thermostat.py | 18 +++++++++--------- .../stateless_exterior_heating.py | 6 +++--- .../tahoma/cover_devices/awning.py | 6 +++--- .../tahoma/cover_devices/tahoma_cover.py | 14 ++++++++------ .../tahoma/cover_devices/vertical_cover.py | 10 +++++++--- custom_components/tahoma/switch.py | 18 +++++++++++------- .../domestic_hot_water_production.py | 8 ++++---- .../tahoma/water_heater_devices/hitachi_dhw.py | 12 ++++++++---- 20 files changed, 109 insertions(+), 83 deletions(-) diff --git a/custom_components/tahoma/alarm_control_panel.py b/custom_components/tahoma/alarm_control_panel.py index f8201f02a..57e08bd97 100644 --- a/custom_components/tahoma/alarm_control_panel.py +++ b/custom_components/tahoma/alarm_control_panel.py @@ -161,19 +161,19 @@ def supported_features(self) -> int: async def async_alarm_disarm(self, code=None): """Send disarm command.""" - await self.async_execute_command( + await self.executor.async_execute_command( self.executor.select_command(COMMAND_DISARM, COMMAND_ALARM_OFF) ) async def async_alarm_arm_home(self, code=None): """Send arm home command.""" - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_ALARM_PARTIAL_1, COMMAND_ARM_PARTIAL_DAY ) async def async_alarm_arm_night(self, code=None): """Send arm night command.""" - await self.async_execute_command( + await self.executor.async_execute_command( self.executor.select_command( COMMAND_PARTIAL, COMMAND_ALARM_PARTIAL_2, COMMAND_ARM_PARTIAL_NIGHT ) @@ -181,19 +181,19 @@ async def async_alarm_arm_night(self, code=None): async def async_alarm_arm_away(self, code=None): """Send arm away command.""" - await self.async_execute_command( + await self.executor.async_execute_command( self.executor.select_command(COMMAND_ARM, COMMAND_ALARM_ON) ) async def async_alarm_trigger(self, code=None) -> None: """Send alarm trigger command.""" - await self.async_execute_command( + await self.executor.async_execute_command( self.executor.select_command(COMMAND_SET_ALARM_STATUS, STATE_DETECTED) ) async def async_alarm_arm_custom_bypass(self, code=None) -> None: """Send arm custom bypass command.""" - await self.async_execute_command( + await self.executor.async_execute_command( self.executor.select_command(COMMAND_SET_ALARM_STATUS, STATE_UNDETECTED) ) diff --git a/custom_components/tahoma/climate_devices/atlantic_electrical_heater.py b/custom_components/tahoma/climate_devices/atlantic_electrical_heater.py index 158bee9c6..6afef3731 100644 --- a/custom_components/tahoma/climate_devices/atlantic_electrical_heater.py +++ b/custom_components/tahoma/climate_devices/atlantic_electrical_heater.py @@ -69,7 +69,7 @@ def hvac_modes(self) -> List[str]: async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_HEATING_LEVEL, HVAC_MODES_TO_TAHOMA[hvac_mode] ) @@ -87,6 +87,6 @@ def preset_modes(self) -> Optional[List[str]]: async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_HEATING_LEVEL, PRESET_MODES_TO_TAHOMA[preset_mode] ) diff --git a/custom_components/tahoma/climate_devices/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py b/custom_components/tahoma/climate_devices/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py index 213d9522b..02caf2fdc 100644 --- a/custom_components/tahoma/climate_devices/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py +++ b/custom_components/tahoma/climate_devices/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py @@ -190,16 +190,16 @@ def hvac_mode(self) -> str: async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if CORE_OPERATING_MODE_STATE in self.device.states: - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_OPERATING_MODE, HVAC_MODE_TO_TAHOMA[hvac_mode] ) else: if hvac_mode == HVAC_MODE_OFF: - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_OFF, ) else: - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_HEATING_LEVEL, PRESET_STATE_COMFORT ) @@ -218,11 +218,11 @@ def preset_mode(self) -> Optional[str]: async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode == PRESET_AUTO or preset_mode == PRESET_PROG: - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_OPERATING_MODE, PRESET_MODE_TO_TAHOMA[preset_mode] ) else: - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_HEATING_LEVEL, PRESET_MODE_TO_TAHOMA[preset_mode] ) @@ -240,4 +240,6 @@ def current_temperature(self): async def async_set_temperature(self, **kwargs) -> None: """Set new temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) - await self.async_execute_command(COMMAND_SET_TARGET_TEMPERATURE, temperature) + await self.executor.async_execute_command( + COMMAND_SET_TARGET_TEMPERATURE, temperature + ) diff --git a/custom_components/tahoma/climate_devices/atlantic_electrical_towel_dryer.py b/custom_components/tahoma/climate_devices/atlantic_electrical_towel_dryer.py index 2cd7daa8c..77564f09b 100644 --- a/custom_components/tahoma/climate_devices/atlantic_electrical_towel_dryer.py +++ b/custom_components/tahoma/climate_devices/atlantic_electrical_towel_dryer.py @@ -94,7 +94,7 @@ def hvac_mode(self) -> str: async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_TOWEL_DRYER_OPERATING_MODE, HVAC_MODE_TO_TAHOMA[hvac_mode] ) @@ -112,7 +112,7 @@ def preset_mode(self) -> Optional[str]: async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_TOWEL_DRYER_TEMPORARY_STATE, PRESET_MODE_TO_TAHOMA[preset_mode] ) @@ -134,10 +134,10 @@ async def async_set_temperature(self, **kwargs) -> None: temperature = kwargs.get(ATTR_TEMPERATURE) if self.hvac_mode == HVAC_MODE_AUTO: - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_DEROGATED_TARGET_TEMPERATURE, temperature ) else: - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_TARGET_TEMPERATURE, temperature ) diff --git a/custom_components/tahoma/climate_devices/atlantic_pass_apc_heating_and_cooling_zone.py b/custom_components/tahoma/climate_devices/atlantic_pass_apc_heating_and_cooling_zone.py index 50d28d9d5..af7331054 100644 --- a/custom_components/tahoma/climate_devices/atlantic_pass_apc_heating_and_cooling_zone.py +++ b/custom_components/tahoma/climate_devices/atlantic_pass_apc_heating_and_cooling_zone.py @@ -177,16 +177,22 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if hvac_mode == HVAC_MODE_OFF: - await self.async_execute_command(COMMAND_SET_HEATING_ON_OFF_STATE, "off") + await self.executor.async_execute_command( + COMMAND_SET_HEATING_ON_OFF_STATE, "off" + ) else: if self.hvac_mode == HVAC_MODE_OFF: - await self.async_execute_command(COMMAND_SET_HEATING_ON_OFF_STATE, "on") + await self.executor.async_execute_command( + COMMAND_SET_HEATING_ON_OFF_STATE, "on" + ) - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_PASS_APC_HEATING_MODE, HVAC_MODE_TO_TAHOMA[hvac_mode] ) - await self.async_execute_command(COMMAND_REFRESH_PASS_APC_HEATING_PROFILE) + await self.executor.async_execute_command( + COMMAND_REFRESH_PASS_APC_HEATING_PROFILE + ) @property def target_temperature(self) -> None: @@ -197,7 +203,7 @@ async def async_set_temperature(self, **kwargs) -> None: """Set new temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_HEATING_TARGET_TEMPERATURE, temperature ) - await self.async_execute_command(COMMAND_REFRESH_TARGET_TEMPERATURE) + await self.executor.async_execute_command(COMMAND_REFRESH_TARGET_TEMPERATURE) diff --git a/custom_components/tahoma/climate_devices/atlantic_pass_apc_zone_control.py b/custom_components/tahoma/climate_devices/atlantic_pass_apc_zone_control.py index 832585b7f..434382edc 100644 --- a/custom_components/tahoma/climate_devices/atlantic_pass_apc_zone_control.py +++ b/custom_components/tahoma/climate_devices/atlantic_pass_apc_zone_control.py @@ -59,6 +59,6 @@ def hvac_mode(self) -> str: async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_PASS_APC_OPERATING_MODE, HVAC_MODE_TO_TAHOMA[hvac_mode] ) diff --git a/custom_components/tahoma/climate_devices/atlantic_pass_apcdhw.py b/custom_components/tahoma/climate_devices/atlantic_pass_apcdhw.py index a1f50ea73..617497637 100644 --- a/custom_components/tahoma/climate_devices/atlantic_pass_apcdhw.py +++ b/custom_components/tahoma/climate_devices/atlantic_pass_apcdhw.py @@ -129,13 +129,15 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: preset_mode_to_set = MAP_REVERSE_PRESET_MODES[PRESET_COMFORT] boost_mode_to_set = BOOST_ON_STATE - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_BOOST_ON_OFF_STATE, boost_mode_to_set ) - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_PASS_APCDHW_MODE, preset_mode_to_set ) - await self.async_execute_command(COMMAND_REFRESH_TARGET_DWH_TEMPERATURE) + await self.executor.async_execute_command( + COMMAND_REFRESH_TARGET_DWH_TEMPERATURE + ) @property def hvac_mode(self) -> str: @@ -149,7 +151,7 @@ def hvac_modes(self) -> List[str]: async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_DWH_ON_OFF_STATE, MAP_REVERSE_HVAC_MODES[hvac_mode] ) @@ -173,13 +175,15 @@ async def async_set_temperature(self, **kwargs) -> None: """Set new temperature for current preset.""" temperature = kwargs.get(ATTR_TEMPERATURE) if self.preset_mode == PRESET_ECO: - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_ECO_TARGET_DWH_TEMPERATURE, temperature ) if self.preset_mode in [PRESET_COMFORT, PRESET_BOOST]: - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_COMFORT_TARGET_DWH_TEMPERATURE, temperature ) - await self.async_execute_command(COMMAND_REFRESH_TARGET_DWH_TEMPERATURE) + await self.executor.async_execute_command( + COMMAND_REFRESH_TARGET_DWH_TEMPERATURE + ) diff --git a/custom_components/tahoma/climate_devices/dimmer_exterior_heating.py b/custom_components/tahoma/climate_devices/dimmer_exterior_heating.py index ca3114194..453939228 100644 --- a/custom_components/tahoma/climate_devices/dimmer_exterior_heating.py +++ b/custom_components/tahoma/climate_devices/dimmer_exterior_heating.py @@ -59,8 +59,8 @@ async def async_set_temperature(self, **kwargs) -> None: level = kwargs.get(ATTR_TEMPERATURE) if level is None: return - await self.async_execute_command(COMMAND_SET_LEVEL, 100 - int(level)) - await self.async_execute_command(COMMAND_GET_LEVEL) + await self.executor.async_execute_command(COMMAND_SET_LEVEL, 100 - int(level)) + await self.executor.async_execute_command(COMMAND_GET_LEVEL) @property def hvac_mode(self) -> str: @@ -81,5 +81,5 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: level = self._saved_level else: self._saved_level = self.target_temperature - await self.async_execute_command(COMMAND_SET_LEVEL, 100 - int(level)) - await self.async_execute_command(COMMAND_GET_LEVEL) + await self.executor.async_execute_command(COMMAND_SET_LEVEL, 100 - int(level)) + await self.executor.async_execute_command(COMMAND_GET_LEVEL) diff --git a/custom_components/tahoma/climate_devices/evo_home_controller.py b/custom_components/tahoma/climate_devices/evo_home_controller.py index 8a73e3f3e..80f0b39b6 100644 --- a/custom_components/tahoma/climate_devices/evo_home_controller.py +++ b/custom_components/tahoma/climate_devices/evo_home_controller.py @@ -64,7 +64,7 @@ def hvac_modes(self) -> List[str]: async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_OPERATING_MODE, HVAC_MODES_TO_TAHOMA[hvac_mode] ) @@ -97,7 +97,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: ) + timedelta(days=7) time_interval = one_week_from_now - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_OPERATING_MODE, PRESET_MODES_TO_TAHOMA[preset_mode], time_interval.strftime("%Y/%m/%d %H:%M"), diff --git a/custom_components/tahoma/climate_devices/heating_set_point.py b/custom_components/tahoma/climate_devices/heating_set_point.py index 59cc25909..5adc86ef2 100644 --- a/custom_components/tahoma/climate_devices/heating_set_point.py +++ b/custom_components/tahoma/climate_devices/heating_set_point.py @@ -86,7 +86,7 @@ def target_temperature(self): async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_TARGET_TEMPERATURE, float(temperature) ) diff --git a/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py b/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py index ba734d1ce..711925f81 100644 --- a/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py +++ b/custom_components/tahoma/climate_devices/hitachi_air_to_air_heat_pump.py @@ -231,7 +231,7 @@ async def _global_control( ): """Execute globalControl command with all parameters.""" if self.device.controllable_name == "ovp:HLinkMainController": - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_GLOBAL_CONTROL, main_operation or self._select_state(*MAIN_OPERATION_STATE), # Main Operation @@ -244,7 +244,7 @@ async def _global_control( swing_mode or self._select_state(*SWING_STATE), # Swing Mode ) else: - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_GLOBAL_CONTROL, main_operation or self._select_state(*MAIN_OPERATION_STATE), # Main Operation diff --git a/custom_components/tahoma/climate_devices/hitachi_air_to_water_heating_zone.py b/custom_components/tahoma/climate_devices/hitachi_air_to_water_heating_zone.py index 34a7e4bc0..479159322 100644 --- a/custom_components/tahoma/climate_devices/hitachi_air_to_water_heating_zone.py +++ b/custom_components/tahoma/climate_devices/hitachi_air_to_water_heating_zone.py @@ -89,7 +89,7 @@ def hvac_modes(self) -> List[str]: async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_AUTO_MANU_MODE, HVAC_MODE_TO_TAHOMA[hvac_mode] ) @@ -107,7 +107,7 @@ def preset_mode(self) -> Optional[str]: async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_TARGET_MODE, PRESET_MODE_TO_TAHOMA[preset_mode] ) @@ -144,6 +144,6 @@ async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_THERMOSTAT_SETTING_CONTROL_ZONE_1, int(temperature) ) diff --git a/custom_components/tahoma/climate_devices/somfy_thermostat.py b/custom_components/tahoma/climate_devices/somfy_thermostat.py index 824322e06..14c5c8399 100644 --- a/custom_components/tahoma/climate_devices/somfy_thermostat.py +++ b/custom_components/tahoma/climate_devices/somfy_thermostat.py @@ -236,13 +236,13 @@ async def async_set_temperature(self, **kwargs) -> None: elif temperature > self.max_temp: temperature = self.max_temp - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_DEROGATION, temperature, STATE_DEROGATION_FURTHER_NOTICE ) - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_MODE_TEMPERATURE, STATE_PRESET_MANUAL, temperature ) - await self.async_execute_command(COMMAND_REFRESH_STATE) + await self.executor.async_execute_command(COMMAND_REFRESH_STATE) async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" @@ -250,8 +250,8 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: return if hvac_mode == HVAC_MODE_AUTO: self._saved_target_temp = self.target_temperature - await self.async_execute_command(COMMAND_EXIT_DEROGATION) - await self.async_execute_command(COMMAND_REFRESH_STATE) + await self.executor.async_execute_command(COMMAND_EXIT_DEROGATION) + await self.executor.async_execute_command(COMMAND_REFRESH_STATE) elif hvac_mode == HVAC_MODE_HEAT: await self.async_set_preset_mode(PRESET_NONE) @@ -261,20 +261,20 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: return if preset_mode in [PRESET_FREEZE, PRESET_NIGHT, PRESET_AWAY, PRESET_HOME]: self._saved_target_temp = self.target_temperature - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_DEROGATION, MAP_REVERSE_PRESET_MODES[preset_mode], STATE_DEROGATION_FURTHER_NOTICE, ) elif preset_mode == PRESET_NONE: - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_DEROGATION, self._saved_target_temp, STATE_DEROGATION_FURTHER_NOTICE, ) - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_MODE_TEMPERATURE, STATE_PRESET_MANUAL, self._saved_target_temp, ) - await self.async_execute_command(COMMAND_REFRESH_STATE) + await self.executor.async_execute_command(COMMAND_REFRESH_STATE) diff --git a/custom_components/tahoma/climate_devices/stateless_exterior_heating.py b/custom_components/tahoma/climate_devices/stateless_exterior_heating.py index ad3e10dee..a0cc2b7e3 100644 --- a/custom_components/tahoma/climate_devices/stateless_exterior_heating.py +++ b/custom_components/tahoma/climate_devices/stateless_exterior_heating.py @@ -47,7 +47,7 @@ def preset_modes(self) -> Optional[List[str]]: async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode in PRESET_MY: - await self.async_execute_command(COMMAND_MY) + await self.executor.async_execute_command(COMMAND_MY) else: _LOGGER.error( "Invalid preset mode %s for device %s", preset_mode, self.name @@ -66,6 +66,6 @@ def hvac_modes(self) -> List[str]: async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if hvac_mode == HVAC_MODE_HEAT: - await self.async_execute_command(COMMAND_ON) + await self.executor.async_execute_command(COMMAND_ON) else: - await self.async_execute_command(COMMAND_OFF) + await self.executor.async_execute_command(COMMAND_OFF) diff --git a/custom_components/tahoma/cover_devices/awning.py b/custom_components/tahoma/cover_devices/awning.py index 7fdcc45b3..440f34020 100644 --- a/custom_components/tahoma/cover_devices/awning.py +++ b/custom_components/tahoma/cover_devices/awning.py @@ -59,12 +59,12 @@ def current_cover_position(self): async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs.get(ATTR_POSITION, 0) - await self.async_execute_command(COMMAND_SET_DEPLOYMENT, position) + await self.executor.async_execute_command(COMMAND_SET_DEPLOYMENT, position) async def async_open_cover(self, **_): """Open the cover.""" - await self.async_execute_command(COMMAND_DEPLOY) + await self.executor.async_execute_command(COMMAND_DEPLOY) async def async_close_cover(self, **_): """Close the cover.""" - await self.async_execute_command(COMMAND_UNDEPLOY) + await self.executor.async_execute_command(COMMAND_UNDEPLOY) diff --git a/custom_components/tahoma/cover_devices/tahoma_cover.py b/custom_components/tahoma/cover_devices/tahoma_cover.py index 1217c6288..0f1726fe0 100644 --- a/custom_components/tahoma/cover_devices/tahoma_cover.py +++ b/custom_components/tahoma/cover_devices/tahoma_cover.py @@ -94,13 +94,13 @@ async def async_set_cover_position_low_speed(self, **kwargs): """Move the cover to a specific position with a low speed.""" position = 100 - kwargs.get(ATTR_POSITION, 0) - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_CLOSURE_AND_LINEAR_SPEED, position, "lowspeed" ) async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - await self.async_execute_command( + await self.executor.async_execute_command( self.executor.select_command(*COMMANDS_SET_TILT_POSITION), 100 - kwargs.get(ATTR_TILT_POSITION, 0), ) @@ -143,13 +143,13 @@ def icon(self): async def async_open_cover_tilt(self, **_): """Open the cover tilt.""" - await self.async_execute_command( + await self.executor.async_execute_command( self.executor.select_command(*COMMANDS_OPEN_TILT) ) async def async_close_cover_tilt(self, **_): """Close the cover tilt.""" - await self.async_execute_command( + await self.executor.async_execute_command( self.executor.select_command(*COMMANDS_CLOSE_TILT) ) @@ -205,11 +205,13 @@ async def async_cancel_or_stop_cover(self, cancel_commands, stop_commands) -> No # Fallback to available stop commands when no executions are found # Stop commands don't work with all devices, due to a bug in Somfy service - await self.async_execute_command(self.executor.select_command(*stop_commands)) + await self.executor.async_execute_command( + self.executor.select_command(*stop_commands) + ) async def async_my(self, **_): """Set cover to preset position.""" - await self.async_execute_command(COMMAND_MY) + await self.executor.async_execute_command(COMMAND_MY) @property def is_opening(self): diff --git a/custom_components/tahoma/cover_devices/vertical_cover.py b/custom_components/tahoma/cover_devices/vertical_cover.py index 58e1a46de..9985342e5 100644 --- a/custom_components/tahoma/cover_devices/vertical_cover.py +++ b/custom_components/tahoma/cover_devices/vertical_cover.py @@ -104,12 +104,16 @@ def current_cover_position(self): async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = 100 - kwargs.get(ATTR_POSITION, 0) - await self.async_execute_command(COMMAND_SET_CLOSURE, position) + await self.executor.async_execute_command(COMMAND_SET_CLOSURE, position) async def async_open_cover(self, **_): """Open the cover.""" - await self.async_execute_command(self.executor.select_command(*COMMANDS_OPEN)) + await self.executor.async_execute_command( + self.executor.select_command(*COMMANDS_OPEN) + ) async def async_close_cover(self, **_): """Close the cover.""" - await self.async_execute_command(self.executor.select_command(*COMMANDS_CLOSE)) + await self.executor.async_execute_command( + self.executor.select_command(*COMMANDS_CLOSE) + ) diff --git a/custom_components/tahoma/switch.py b/custom_components/tahoma/switch.py index f4a9af464..77f59a78a 100644 --- a/custom_components/tahoma/switch.py +++ b/custom_components/tahoma/switch.py @@ -72,13 +72,15 @@ def icon(self) -> Optional[str]: async def async_turn_on(self, **_): """Send the on command.""" if self.executor.has_command(COMMAND_ON): - await self.async_execute_command(COMMAND_ON) + await self.executor.async_execute_command(COMMAND_ON) elif self.executor.has_command(COMMAND_SET_FORCE_HEATING): - await self.async_execute_command(COMMAND_SET_FORCE_HEATING, STATE_ON) + await self.executor.async_execute_command( + COMMAND_SET_FORCE_HEATING, STATE_ON + ) elif self.executor.has_command(COMMAND_RING_WITH_SINGLE_SIMPLE_SEQUENCE): - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_RING_WITH_SINGLE_SIMPLE_SEQUENCE, # https://www.tahomalink.com/enduser-mobile-web/steer-html5-client/vendor/somfy/io/siren/const.js 2 * 60 * 1000, # 2 minutes 75, # 90 seconds bip, 30 seconds silence @@ -89,7 +91,7 @@ async def async_turn_on(self, **_): async def async_turn_off(self, **_): """Send the off command.""" if self.executor.has_command(COMMAND_RING_WITH_SINGLE_SIMPLE_SEQUENCE): - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_RING_WITH_SINGLE_SIMPLE_SEQUENCE, 2000, 100, @@ -98,15 +100,17 @@ async def async_turn_off(self, **_): ) elif self.executor.has_command(COMMAND_SET_FORCE_HEATING): - await self.async_execute_command(COMMAND_SET_FORCE_HEATING, STATE_OFF) + await self.executor.async_execute_command( + COMMAND_SET_FORCE_HEATING, STATE_OFF + ) elif self.executor.has_command(COMMAND_OFF): - await self.async_execute_command(COMMAND_OFF) + await self.executor.async_execute_command(COMMAND_OFF) async def async_toggle(self, **_): """Click the switch.""" if self.executor.has_command(COMMAND_CYCLE): - await self.async_execute_command(COMMAND_CYCLE) + await self.executor.async_execute_command(COMMAND_CYCLE) @property def is_on(self): diff --git a/custom_components/tahoma/water_heater_devices/domestic_hot_water_production.py b/custom_components/tahoma/water_heater_devices/domestic_hot_water_production.py index 9e6ae3a47..ae642322f 100644 --- a/custom_components/tahoma/water_heater_devices/domestic_hot_water_production.py +++ b/custom_components/tahoma/water_heater_devices/domestic_hot_water_production.py @@ -91,13 +91,13 @@ def target_temperature_low(self): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" target_temperature = kwargs.get(ATTR_TEMPERATURE) - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_TARGET_TEMPERATURE, target_temperature ) async def async_set_operation_mode(self, operation_mode): """Set new target operation mode.""" - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_DHW_MODE, MAP_REVERSE_OPERATION_MODES[operation_mode] ) @@ -116,7 +116,7 @@ def is_away_mode_on(self): async def async_turn_away_mode_on(self): """Turn away mode on.""" - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_CURRENT_OPERATING_MODE, { STATE_RELAUNCH: STATE_OFF, @@ -126,7 +126,7 @@ async def async_turn_away_mode_on(self): async def async_turn_away_mode_off(self): """Turn away mode off.""" - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_CURRENT_OPERATING_MODE, { STATE_RELAUNCH: STATE_OFF, diff --git a/custom_components/tahoma/water_heater_devices/hitachi_dhw.py b/custom_components/tahoma/water_heater_devices/hitachi_dhw.py index 97c1a2fbc..2cebf4b01 100644 --- a/custom_components/tahoma/water_heater_devices/hitachi_dhw.py +++ b/custom_components/tahoma/water_heater_devices/hitachi_dhw.py @@ -82,7 +82,7 @@ def target_temperature(self): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_CONTROL_DHW_SETTING_TEMPERATURE, int(temperature) ) @@ -105,13 +105,17 @@ async def async_set_operation_mode(self, operation_mode): """Set new target operation mode.""" # Turn water heater off if operation_mode == STATE_OFF: - return await self.async_execute_command(COMMAND_SET_CONTROL_DHW, STATE_STOP) + return await self.executor.async_execute_command( + COMMAND_SET_CONTROL_DHW, STATE_STOP + ) # Turn water heater on, when off if self.current_operation == STATE_OFF and operation_mode != STATE_OFF: - await self.async_execute_command(COMMAND_SET_CONTROL_DHW, STATE_RUN) + await self.executor.async_execute_command( + COMMAND_SET_CONTROL_DHW, STATE_RUN + ) # Change operation mode - await self.async_execute_command( + await self.executor.async_execute_command( COMMAND_SET_DHW_MODE, OPERATION_MODE_TO_TAHOMA[operation_mode] ) From d146ad6cefc988fe80feb10bb5d4c1593984ee37 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 10 Aug 2021 11:13:19 +0200 Subject: [PATCH 4/6] Squashed commit of the following: commit 9505d8c2b729ebdf69704a2683f3276f029fc7c3 Author: Mick Vleeshouwer Date: Tue Aug 10 02:03:19 2021 -0700 Add sensors dynamically based on available device state (#425) * Add temporary state sensor * Add extra sensors * Add more states * Add more sensors * Create supported_platforms variable * Use Overkiz label as device name * Suffix state sensor name with device index * Avoid to duplicate state key * Remove now usless rssi level attribute * Migrate LightSensor to TahomaStateSensor * Apply linters * Add PriorityLockOriginatorState sensor * Bump minimum version to 2021.8 * Remove unneeded value key * Migrate to SensorEntityDescription * Small tweaks * Add battery level * Remove unused state keys * Test electricity sensor * Add extra sensors * Add sensor * Add all sensors from sensor.py * Add smoke text sensor * Make optional * Change all sensor to new structure * Improve sensor definitions * Ease state * Ease retrieve of the index * Rollback device_state_attributes * Revert battery changes * Remove const * Add LightSensor * Rollback icon logic in cover * Bugfixes * Add round for RSSI value Co-authored-by: Thibaut Etienne commit e7a45543be68ac3e139e72717e05fe4a7c37c658 Author: Mick Vleeshouwer Date: Tue Aug 10 01:56:39 2021 -0700 Add binary sensors dynamically based on available device state (#505) * Refactor binary sensor * Make import relative * Style fix * Update custom_components/tahoma/__init__.py Co-authored-by: Thibaut * Add extra binary sensors * Clean for final PR * Feedback applied Co-authored-by: Thibaut commit da5b26ed26096446dd9686421c9249328c72b106 Author: Mick Vleeshouwer Date: Tue Aug 10 01:34:16 2021 -0700 Temporary bugfix for AwningValance (until this is fixed serverside) (#511) * Temporary bugfix for https://github.com/iMicknl/ha-tahoma/issues/486 * Add note --- custom_components/tahoma/__init__.py | 11 +- custom_components/tahoma/binary_sensor.py | 196 ++++---- custom_components/tahoma/const.py | 50 +- custom_components/tahoma/cover.py | 6 +- .../tahoma/cover_devices/awning.py | 5 +- .../tahoma/cover_devices/tahoma_cover.py | 6 +- .../tahoma/cover_devices/vertical_cover.py | 5 +- custom_components/tahoma/sensor.py | 476 ++++++++++++------ hacs.json | 2 +- requirements_dev.txt | 2 +- requirements_test.txt | 2 +- 11 files changed, 485 insertions(+), 276 deletions(-) diff --git a/custom_components/tahoma/__init__.py b/custom_components/tahoma/__init__.py index 03cfd9565..97dbff12d 100644 --- a/custom_components/tahoma/__init__.py +++ b/custom_components/tahoma/__init__.py @@ -6,7 +6,9 @@ import logging 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 @@ -186,6 +188,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "update_listener": entry.add_update_listener(update_listener), } + # Map Overkiz device to Home Assistant platform for device in tahoma_coordinator.data.values(): platform = TAHOMA_DEVICE_TO_PLATFORM.get( device.widget @@ -202,7 +205,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if device.widget == HOMEKIT_STACK: print_homekit_setup_code(device) - for platform in platforms: + supported_platforms = set(platforms.keys()) + + # 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( hass.config_entries.async_forward_entry_setup(entry, platform) ) diff --git a/custom_components/tahoma/binary_sensor.py b/custom_components/tahoma/binary_sensor.py index a9ea574fa..db7e08ab0 100644 --- a/custom_components/tahoma/binary_sensor.py +++ b/custom_components/tahoma/binary_sensor.py @@ -1,64 +1,94 @@ """Support for TaHoma binary sensors.""" -from typing import Optional +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable + +from homeassistant.components import binary_sensor from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_SMOKE, - DOMAIN as BINARY_SENSOR, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .tahoma_entity import OverkizEntity - -CORE_ASSEMBLY_STATE = "core:AssemblyState" -CORE_BUTTON_STATE = "core:ButtonState" -CORE_CONTACT_STATE = "core:ContactState" -CORE_GAS_DETECTION_STATE = "core:GasDetectionState" -CORE_OCCUPANCY_STATE = "core:OccupancyState" -CORE_OPENING_STATE = "core:OpeningState" -CORE_OPEN_CLOSED_TILT_STATE = "core:OpenClosedTiltState" -CORE_RAIN_STATE = "core:RainState" -CORE_SMOKE_STATE = "core:SmokeState" -CORE_THREE_WAY_HANDLE_DIRECTION_STATE = "core:ThreeWayHandleDirectionState" -CORE_VIBRATION_STATE = "core:VibrationState" -CORE_WATER_DETECTION_STATE = "core:WaterDetectionState" - -DEVICE_CLASS_BUTTON = "button" -DEVICE_CLASS_GAS = "gas" -DEVICE_CLASS_RAIN = "rain" -DEVICE_CLASS_WATER = "water" - -ICON_WATER = "mdi:water" -ICON_WATER_OFF = "mdi:water-off" -ICON_WAVES = "mdi:waves" -ICON_WEATHER_RAINY = "mdi:weather-rainy" - -IO_VIBRATION_DETECTED_STATE = "io:VibrationDetectedState" +from .coordinator import TahomaDataUpdateCoordinator +from .entity import OverkizEntity STATE_OPEN = "open" STATE_PERSON_INSIDE = "personInside" STATE_DETECTED = "detected" -STATE_PRESSED = "pressed" - -TAHOMA_BINARY_SENSOR_DEVICE_CLASSES = { - "AirFlowSensor": DEVICE_CLASS_GAS, - "CarButtonSensor": DEVICE_CLASS_BUTTON, - "ContactSensor": DEVICE_CLASS_OPENING, - "MotionSensor": DEVICE_CLASS_MOTION, - "OccupancySensor": DEVICE_CLASS_OCCUPANCY, - "RainSensor": DEVICE_CLASS_RAIN, - "SirenStatus": DEVICE_CLASS_OPENING, - "SmokeSensor": DEVICE_CLASS_SMOKE, - "WaterDetectionSensor": DEVICE_CLASS_WATER, - "WaterSensor": DEVICE_CLASS_WATER, - "WindowHandle": DEVICE_CLASS_OPENING, -} + + +@dataclass +class OverkizBinarySensorDescription(BinarySensorEntityDescription): + """Class to describe a Overkiz binary sensor.""" + + is_on: Callable[[Any], Any] = lambda state: state + + +BINARY_SENSOR_DESCRIPTIONS = [ + # RainSensor/RainSensor + OverkizBinarySensorDescription( + key="core:RainState", + name="Rain", + icon="mdi:weather-rainy", + is_on=lambda state: state == STATE_DETECTED, + ), + # SmokeSensor/SmokeSensor + OverkizBinarySensorDescription( + key="core:SmokeState", + name="Smoke", + device_class=binary_sensor.DEVICE_CLASS_SMOKE, + is_on=lambda state: state == STATE_DETECTED, + ), + # WaterSensor/WaterDetectionSensor + OverkizBinarySensorDescription( + key="core:WaterDetectionState", + name="Water", + icon="mdi:water", + is_on=lambda state: state == STATE_DETECTED, + ), + # AirSensor/AirFlowSensor + OverkizBinarySensorDescription( + key="core:GasDetectionState", + name="Gas", + device_class=binary_sensor.DEVICE_CLASS_GAS, + is_on=lambda state: state == STATE_DETECTED, + ), + # OccupancySensor/OccupancySensor + # OccupancySensor/MotionSensor + OverkizBinarySensorDescription( + key="core:OccupancyState", + name="Occupancy", + device_class=binary_sensor.DEVICE_CLASS_OCCUPANCY, + is_on=lambda state: state == STATE_PERSON_INSIDE, + ), + # ContactSensor/WindowWithTiltSensor + OverkizBinarySensorDescription( + key="core:VibrationState", + name="Vibration", + device_class=binary_sensor.DEVICE_CLASS_VIBRATION, + is_on=lambda state: state == STATE_DETECTED, + ), + # ContactSensor/ContactSensor + OverkizBinarySensorDescription( + key="core:ContactState", + name="Contact", + device_class=binary_sensor.DEVICE_CLASS_DOOR, + is_on=lambda state: state == STATE_OPEN, + ), + # Unknown + OverkizBinarySensorDescription( + key="io:VibrationDetectedState", + name="Vibration", + device_class=binary_sensor.DEVICE_CLASS_VIBRATION, + is_on=lambda state: state == STATE_DETECTED, + ), +] async def async_setup_entry( @@ -69,55 +99,43 @@ async def async_setup_entry( """Set up the TaHoma sensors from a config entry.""" data = hass.data[DOMAIN][entry.entry_id] coordinator = data["coordinator"] + entities = [] + + key_supported_states = { + description.key: description for description in BINARY_SENSOR_DESCRIPTIONS + } + + for device in coordinator.data.values(): + for state in device.states: + description = key_supported_states.get(state.name) + + if description: + entities.append( + TahomaBinarySensor( + device.deviceurl, + coordinator, + description, + ) + ) - entities = [ - TahomaBinarySensor(device.deviceurl, coordinator) - for device in data["platforms"][BINARY_SENSOR] - ] async_add_entities(entities) class TahomaBinarySensor(OverkizEntity, BinarySensorEntity): """Representation of a TaHoma Binary Sensor.""" + def __init__( + self, + device_url: str, + coordinator: TahomaDataUpdateCoordinator, + description: OverkizBinarySensorDescription, + ): + """Initialize the device.""" + super().__init__(device_url, coordinator) + self.entity_description = description + @property def is_on(self): """Return the state of the sensor.""" - - return ( - self.executor.select_state( - CORE_ASSEMBLY_STATE, - CORE_BUTTON_STATE, - CORE_CONTACT_STATE, - CORE_GAS_DETECTION_STATE, - CORE_OCCUPANCY_STATE, - CORE_OPENING_STATE, - CORE_OPEN_CLOSED_TILT_STATE, - CORE_RAIN_STATE, - CORE_SMOKE_STATE, - CORE_THREE_WAY_HANDLE_DIRECTION_STATE, - CORE_VIBRATION_STATE, - CORE_WATER_DETECTION_STATE, - IO_VIBRATION_DETECTED_STATE, - ) - in [STATE_OPEN, STATE_PERSON_INSIDE, STATE_DETECTED, STATE_PRESSED] - ) - - @property - def device_class(self): - """Return the class of the device.""" - return TAHOMA_BINARY_SENSOR_DEVICE_CLASSES.get( - self.device.widget - ) or TAHOMA_BINARY_SENSOR_DEVICE_CLASSES.get(self.device.ui_class) - - @property - def icon(self) -> Optional[str]: - """Return the icon to use in the frontend, if any.""" - if self.device_class == DEVICE_CLASS_WATER: - if self.is_on: - return ICON_WATER - return ICON_WATER_OFF - - icons = {DEVICE_CLASS_GAS: ICON_WAVES, DEVICE_CLASS_RAIN: ICON_WEATHER_RAINY} - - return icons.get(self.device_class) + state = self.device.states[self.entity_description.key] + return self.entity_description.is_on(state) diff --git a/custom_components/tahoma/const.py b/custom_components/tahoma/const.py index dfe2bcaae..37d0232cf 100644 --- a/custom_components/tahoma/const.py +++ b/custom_components/tahoma/const.py @@ -1,11 +1,9 @@ """Constants for the TaHoma integration.""" from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_CONTROL_PANEL -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.climate import DOMAIN as CLIMATE 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 @@ -20,13 +18,34 @@ 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", + "MotionSensor", + "OccupancySensor", + "RainSensor", + "SmokeSensor", + "WaterDetectionSensor", # widgetName, uiClass is HumiditySensor (sensor) ] # Used to map the Somfy widget and ui_class to the Home Assistant platform TAHOMA_DEVICE_TO_PLATFORM = { "AdjustableSlatsRollerShutter": COVER, - "AirFlowSensor": BINARY_SENSOR, # widgetName, uiClass is AirSensor (sensor) - "AirSensor": SENSOR, "Alarm": ALARM_CONTROL_PANEL, "AtlanticElectricalHeater": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) "AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) @@ -35,57 +54,36 @@ "AtlanticPassAPCHeatingAndCoolingZone": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) "AtlanticPassAPCZoneControl": CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) "Awning": COVER, - "CarButtonSensor": BINARY_SENSOR, - "ConsumptionSensor": SENSOR, - "ContactSensor": BINARY_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, - "MotionSensor": BINARY_SENSOR, "MyFoxSecurityCamera": COVER, # widgetName, uiClass is Camera (not supported) - "OccupancySensor": BINARY_SENSOR, "OnOff": SWITCH, "Pergola": COVER, - "RainSensor": BINARY_SENSOR, "RollerShutter": COVER, "RTSGeneric": COVER, # widgetName, uiClass is Generic (not supported) "Screen": COVER, "Shutter": COVER, "Siren": SWITCH, - "SirenStatus": BINARY_SENSOR, # widgetName, uiClass is Siren (switch) - "SmokeSensor": BINARY_SENSOR, + "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, - "WaterDetectionSensor": BINARY_SENSOR, # widgetName, uiClass is HumiditySensor (sensor) - "WaterSensor": SENSOR, - "WeatherSensor": SENSOR, - "WindSensor": SENSOR, "Window": COVER, - "WindowHandle": BINARY_SENSOR, } CORE_ON_OFF_STATE = "core:OnOffState" diff --git a/custom_components/tahoma/cover.py b/custom_components/tahoma/cover.py index d03bee1bc..607e5bc44 100644 --- a/custom_components/tahoma/cover.py +++ b/custom_components/tahoma/cover.py @@ -24,16 +24,18 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] coordinator = data["coordinator"] + # Includes fix for #486, which is waiting on Somfy back-end deployment + # Remove when DeploymentState will be returned for AwningValance entities = [ Awning(device.deviceurl, coordinator) for device in data["platforms"].get(COVER) - if device.ui_class == "Awning" + if device.ui_class == "Awning" and device.widget != "AwningValance" ] entities += [ VerticalCover(device.deviceurl, coordinator) for device in data["platforms"].get(COVER) - if device.ui_class != "Awning" + if device.ui_class != "Awning" or device.widget == "AwningValance" ] async_add_entities(entities) diff --git a/custom_components/tahoma/cover_devices/awning.py b/custom_components/tahoma/cover_devices/awning.py index 440f34020..ac043ba46 100644 --- a/custom_components/tahoma/cover_devices/awning.py +++ b/custom_components/tahoma/cover_devices/awning.py @@ -8,10 +8,7 @@ SUPPORT_STOP, ) -from custom_components.tahoma.cover_devices.tahoma_cover import ( - COMMANDS_STOP, - TahomaGenericCover, -) +from .tahoma_cover import COMMANDS_STOP, TahomaGenericCover COMMAND_DEPLOY = "deploy" COMMAND_SET_DEPLOYMENT = "setDeployment" diff --git a/custom_components/tahoma/cover_devices/tahoma_cover.py b/custom_components/tahoma/cover_devices/tahoma_cover.py index 0f1726fe0..a08ed0619 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" diff --git a/custom_components/tahoma/cover_devices/vertical_cover.py b/custom_components/tahoma/cover_devices/vertical_cover.py index 9985342e5..25497c67e 100644 --- a/custom_components/tahoma/cover_devices/vertical_cover.py +++ b/custom_components/tahoma/cover_devices/vertical_cover.py @@ -14,10 +14,7 @@ SUPPORT_STOP, ) -from custom_components.tahoma.cover_devices.tahoma_cover import ( - COMMANDS_STOP, - TahomaGenericCover, -) +from .tahoma_cover import COMMANDS_STOP, TahomaGenericCover COMMAND_CYCLE = "cycle" COMMAND_CLOSE = "close" diff --git a/custom_components/tahoma/sensor.py b/custom_components/tahoma/sensor.py index df27caf23..6fd3bba2c 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 .tahoma_entity import OverkizEntity +from .coordinator import TahomaDataUpdateCoordinator +from .entity import OverkizEntity _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(OverkizEntity, Entity): +class TahomaStateSensor(OverkizEntity, 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.executor.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/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 From 51d014e6f4d76de7b31a495be46e1eb4d6e045e7 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 10 Aug 2021 11:18:23 +0200 Subject: [PATCH 5/6] Bugfix --- custom_components/tahoma/executor.py | 12 +++--------- custom_components/tahoma/sensor.py | 4 ++-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/custom_components/tahoma/executor.py b/custom_components/tahoma/executor.py index 4a3a8be9f..9fccb3ed4 100644 --- a/custom_components/tahoma/executor.py +++ b/custom_components/tahoma/executor.py @@ -17,7 +17,9 @@ def __init__(self, device_url: str, coordinator: TahomaDataUpdateCoordinator): """Initialize the executor.""" self.device_url = device_url self.coordinator = coordinator - self.base_device_url = self.get_base_device_url() + self.device_url = device_url + self.base_device_url, *index = self.device_url.split("#") + self.index = index[0] if index else None @property def device(self) -> Device: @@ -94,11 +96,3 @@ def get_gateway_id(self): """ url = urlparse(self.device_url) return url.netloc - - 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 diff --git a/custom_components/tahoma/sensor.py b/custom_components/tahoma/sensor.py index 6fd3bba2c..b7ca86d29 100644 --- a/custom_components/tahoma/sensor.py +++ b/custom_components/tahoma/sensor.py @@ -367,8 +367,8 @@ def state(self): @property def name(self) -> str: """Return the name of the device.""" - if self.index: - return f"{self.entity_description.name} {self.index}" + if self.executor.index: + return f"{self.entity_description.name} {self.executor.index}" return self.entity_description.name @property From e86605791ea538b19286f89d584fd0c594dc4a27 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 10 Aug 2021 11:24:30 +0200 Subject: [PATCH 6/6] Remove --- custom_components/tahoma/tahoma_entity.py | 204 ---------------------- 1 file changed, 204 deletions(-) delete mode 100644 custom_components/tahoma/tahoma_entity.py diff --git a/custom_components/tahoma/tahoma_entity.py b/custom_components/tahoma/tahoma_entity.py deleted file mode 100644 index d4fc39e65..000000000 --- a/custom_components/tahoma/tahoma_entity.py +++ /dev/null @@ -1,204 +0,0 @@ -"""Parent class for every TaHoma device.""" -import logging -import re -from typing import Any, Dict, Optional - -from homeassistant.const import ATTR_BATTERY_LEVEL -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from pyhoma.models import Command, Device - -from .const import DOMAIN -from .coordinator import TahomaDataUpdateCoordinator - -CORE_MANUFACTURER = "core:Manufacturer" -CORE_MANUFACTURER_NAME_STATE = "core:ManufacturerNameState" -CORE_MODEL_STATE = "core:ModelState" -CORE_PRODUCT_MODEL_NAME_STATE = "core:ProductModelNameState" - -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" -STATE_BATTERY_LOW = "low" -STATE_BATTERY_VERY_LOW = "verylow" -STATE_DEAD = "dead" - -BATTERY_MAP = { - STATE_BATTERY_FULL: 100, - STATE_BATTERY_NORMAL: 75, - STATE_BATTERY_LOW: 25, - STATE_BATTERY_VERY_LOW: 10, -} - -_LOGGER = logging.getLogger(__name__) - - -class TahomaEntity(CoordinatorEntity, Entity): - """Representation of a TaHoma device entity.""" - - def __init__(self, device_url: str, coordinator: TahomaDataUpdateCoordinator): - """Initialize the device.""" - super().__init__(coordinator) - self.device_url = device_url - self.base_device_url, *index = self.device_url.split("#") - self.index = index[0] if index else None - - @property - def device(self) -> Device: - """Return TaHoma device linked to this entity.""" - return self.coordinator.data[self.device_url] - - @property - def name(self) -> str: - """Return the name of the device.""" - return self.device.label - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self.device.available - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self.device.deviceurl - - @property - def assumed_state(self) -> bool: - """Return True if unable to access real state of the entity.""" - return not self.device.states - - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the state attributes of the device.""" - attr = {} - - 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) - - if self.select_state(CORE_SENSOR_DEFECT_STATE) == STATE_DEAD: - attr[ATTR_BATTERY_LEVEL] = 0 - - if self.device.attributes: - for attribute in self.device.attributes: - attr[attribute.name] = attribute.value - - if self.device.states: - for state in self.device.states: - if "State" in state.name: - attr[state.name] = state.value - - return attr - - @property - def device_info(self) -> Dict[str, Any]: - """Return device registry information for this entity.""" - # Some devices, such as the Smart Thermostat have several devices in one physical device, - # with same device url, terminated by '#' and a number. - # In this case, we use the base device url as the device identifier. - if "#" in self.device_url and not self.device_url.endswith("#1"): - # Only return the url of the base device, to inherit device name and model from parent device. - return { - "identifiers": {(DOMAIN, self.base_device_url)}, - } - - manufacturer = ( - self.select_attribute(CORE_MANUFACTURER) - or self.select_state(CORE_MANUFACTURER_NAME_STATE) - or "Somfy" - ) - model = ( - self.select_state( - CORE_MODEL_STATE, CORE_PRODUCT_MODEL_NAME_STATE, IO_MODEL_STATE - ) - or self.device.widget - ) - - return { - "identifiers": {(DOMAIN, self.base_device_url)}, - "name": self.device.label, - "manufacturer": manufacturer, - "model": model, - "sw_version": self.device.controllable_name, - "via_device": self.get_gateway_id(), - "suggested_area": self.coordinator.areas[self.device.placeoid], - } - - def select_command(self, *commands: str) -> Optional[str]: - """Select first existing command in a list of commands.""" - existing_commands = self.device.definition.commands - - return next((c for c in commands if c in existing_commands), None) - - def has_command(self, *commands: str) -> bool: - """Return True if a command exists in a list of commands.""" - return self.select_command(*commands) is not None - - def select_state(self, *states) -> Optional[str]: - """Select first existing active state in a list of states.""" - if self.device.states: - return next( - ( - state.value - for state in self.device.states - if state.name in list(states) - ), - None, - ) - return None - - def has_state(self, *states: str) -> bool: - """Return True if a state exists in self.""" - return self.select_state(*states) is not None - - def select_attribute(self, *attributes) -> Optional[str]: - """Select first existing active state in a list of states.""" - if self.device.attributes: - return next( - ( - attribute.value - for attribute in self.device.attributes - if attribute.name in list(attributes) - ), - None, - ) - - async def async_execute_command(self, command_name: str, *args: Any): - """Execute device command in async context.""" - try: - exec_id = await self.coordinator.client.execute_command( - self.device.deviceurl, - Command(command_name, list(args)), - "Home Assistant", - ) - except Exception as exception: # pylint: disable=broad-except - _LOGGER.error(exception) - return - - # ExecutionRegisteredEvent doesn't contain the deviceurl, thus we need to register it here - self.coordinator.executions[exec_id] = { - "deviceurl": self.device.deviceurl, - "command_name": command_name, - } - - await self.coordinator.async_refresh() - - 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_gateway_id(self): - """Retrieve gateway id from device url.""" - result = re.search(r":\/\/(.*)\/", self.device_url) - - if result: - return result.group(1) - else: - return None