From 1a5eeb2db11db68d5da59830503b6f3f823ec5e5 Mon Sep 17 00:00:00 2001 From: Nyro Date: Fri, 4 Nov 2022 10:21:30 +0100 Subject: [PATCH] Add Overkiz AtlanticPassAPCHeatingAndCoolingZone (#78659) * Add Overkiz AtlanticPassAPCHeatingAndCoolingZone * Fix commands instanciations to be simpler * Update AtlanticPassAPCHeatingAndCoolingZone to show temperature and fix HA threads * Simplify async_execute_commands in order to receive simpler list * Fix get and set temperature in derogation or auto mode * Remove hvac_action from AtlanticPassAPCHeatingAndCoolingZone * Remove unused lines * Update async_execute_commands to work like async_execute_command Implement cancel for multiple commands * Improve to use preset_home for internal scheduling * Remove async_execute_commands * Improvement for AtlanticPassAPCHeatingAndCoolingZone * Update homeassistant/components/overkiz/climate_entities/__init__.py Co-authored-by: Quentame * Update homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py Co-authored-by: Quentame * Update homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py Co-authored-by: Quentame * Update homeassistant/components/overkiz/const.py Co-authored-by: Quentame Co-authored-by: Quentame --- .../overkiz/climate_entities/__init__.py | 4 + ...antic_pass_apc_heating_and_cooling_zone.py | 203 ++++++++++++++++++ homeassistant/components/overkiz/const.py | 1 + homeassistant/components/overkiz/entity.py | 5 +- 4 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py diff --git a/homeassistant/components/overkiz/climate_entities/__init__.py b/homeassistant/components/overkiz/climate_entities/__init__.py index 32fae234be1..359f7629a7b 100644 --- a/homeassistant/components/overkiz/climate_entities/__init__.py +++ b/homeassistant/components/overkiz/climate_entities/__init__.py @@ -4,6 +4,9 @@ from pyoverkiz.enums.ui import UIWidget from .atlantic_electrical_heater import AtlanticElectricalHeater from .atlantic_electrical_towel_dryer import AtlanticElectricalTowelDryer from .atlantic_heat_recovery_ventilation import AtlanticHeatRecoveryVentilation +from .atlantic_pass_apc_heating_and_cooling_zone import ( + AtlanticPassAPCHeatingAndCoolingZone, +) from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl from .somfy_thermostat import SomfyThermostat @@ -11,6 +14,7 @@ WIDGET_TO_CLIMATE_ENTITY = { UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater, UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: AtlanticElectricalTowelDryer, UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: AtlanticHeatRecoveryVentilation, + UIWidget.ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: AtlanticPassAPCHeatingAndCoolingZone, UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: AtlanticPassAPCZoneControl, UIWidget.SOMFY_THERMOSTAT: SomfyThermostat, } diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py new file mode 100644 index 00000000000..efdbf94d9f7 --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py @@ -0,0 +1,203 @@ +"""Support for Atlantic Pass APC Heating And Cooling Zone Control.""" +from __future__ import annotations + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.climate import ( + PRESET_AWAY, + PRESET_COMFORT, + PRESET_ECO, + PRESET_HOME, + PRESET_SLEEP, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +from ..coordinator import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + +OVERKIZ_TO_HVAC_MODE: dict[str, str] = { + OverkizCommandParam.AUTO: HVACMode.AUTO, + OverkizCommandParam.ECO: HVACMode.AUTO, + OverkizCommandParam.MANU: HVACMode.HEAT, + OverkizCommandParam.HEATING: HVACMode.HEAT, + OverkizCommandParam.STOP: HVACMode.OFF, + OverkizCommandParam.INTERNAL_SCHEDULING: HVACMode.AUTO, + OverkizCommandParam.COMFORT: HVACMode.HEAT, +} + +HVAC_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODE.items()} + +OVERKIZ_TO_PRESET_MODES: dict[str, str] = { + OverkizCommandParam.OFF: PRESET_ECO, + OverkizCommandParam.STOP: PRESET_ECO, + OverkizCommandParam.MANU: PRESET_COMFORT, + OverkizCommandParam.COMFORT: PRESET_COMFORT, + OverkizCommandParam.ABSENCE: PRESET_AWAY, + OverkizCommandParam.ECO: PRESET_ECO, + OverkizCommandParam.INTERNAL_SCHEDULING: PRESET_HOME, +} + +PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODES.items()} + +OVERKIZ_TO_PROFILE_MODES: dict[str, str] = { + OverkizCommandParam.OFF: PRESET_SLEEP, + OverkizCommandParam.STOP: PRESET_SLEEP, + OverkizCommandParam.ECO: PRESET_ECO, + OverkizCommandParam.ABSENCE: PRESET_AWAY, + OverkizCommandParam.MANU: PRESET_COMFORT, + OverkizCommandParam.DEROGATION: PRESET_COMFORT, + OverkizCommandParam.COMFORT: PRESET_COMFORT, +} + +OVERKIZ_TEMPERATURE_STATE_BY_PROFILE: dict[str, str] = { + OverkizCommandParam.ECO: OverkizState.CORE_ECO_HEATING_TARGET_TEMPERATURE, + OverkizCommandParam.COMFORT: OverkizState.CORE_COMFORT_HEATING_TARGET_TEMPERATURE, + OverkizCommandParam.DEROGATION: OverkizState.CORE_DEROGATED_TARGET_TEMPERATURE, +} + + +class AtlanticPassAPCHeatingAndCoolingZone(OverkizEntity, ClimateEntity): + """Representation of Atlantic Pass APC Heating And Cooling Zone Control.""" + + _attr_hvac_modes = [*HVAC_MODE_TO_OVERKIZ] + _attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ) + _attr_temperature_unit = TEMP_CELSIUS + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + + # Temperature sensor use the same base_device_url and use the n+1 index + self.temperature_device = self.executor.linked_device( + int(self.index_device_url) + 1 + ) + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + return cast(float, temperature.value) + + return None + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + return OVERKIZ_TO_HVAC_MODE[ + cast(str, self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_MODE)) + ] + + @property + def current_heating_profile(self) -> str: + """Return current heating profile.""" + return cast( + str, + self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_PROFILE), + ) + + async def async_set_heating_mode(self, mode: str) -> None: + """Set new heating mode and refresh states.""" + await self.executor.async_execute_command( + OverkizCommand.SET_PASS_APC_HEATING_MODE, mode + ) + + if self.current_heating_profile == OverkizCommandParam.DEROGATION: + # If current mode is in derogation, disable it + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATION_ON_OFF_STATE, OverkizCommandParam.OFF + ) + + # We also needs to execute these 2 commands to make it work correctly + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_MODE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_PROFILE + ) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + await self.async_set_heating_mode(HVAC_MODE_TO_OVERKIZ[hvac_mode]) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + await self.async_set_heating_mode(PRESET_MODES_TO_OVERKIZ[preset_mode]) + + @property + def preset_mode(self) -> str: + """Return the current preset mode, e.g., home, away, temp.""" + heating_mode = cast( + str, self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_MODE) + ) + + if heating_mode == OverkizCommandParam.INTERNAL_SCHEDULING: + # In Internal scheduling, it could be comfort or eco + return OVERKIZ_TO_PROFILE_MODES[ + cast( + str, + self.executor.select_state( + OverkizState.IO_PASS_APC_HEATING_PROFILE + ), + ) + ] + + return OVERKIZ_TO_PRESET_MODES[heating_mode] + + @property + def target_temperature(self) -> float: + """Return hvac target temperature.""" + current_heating_profile = self.current_heating_profile + if current_heating_profile in OVERKIZ_TEMPERATURE_STATE_BY_PROFILE: + return cast( + float, + self.executor.select_state( + OVERKIZ_TEMPERATURE_STATE_BY_PROFILE[current_heating_profile] + ), + ) + return cast( + float, self.executor.select_state(OverkizState.CORE_TARGET_TEMPERATURE) + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + + if self.hvac_mode == HVACMode.AUTO: + await self.executor.async_execute_command( + OverkizCommand.SET_COMFORT_HEATING_TARGET_TEMPERATURE, + temperature, + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_COMFORT_HEATING_TARGET_TEMPERATURE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_TARGET_TEMPERATURE + ) + else: + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATED_TARGET_TEMPERATURE, + temperature, + ) + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATION_ON_OFF_STATE, + OverkizCommandParam.ON, + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_TARGET_TEMPERATURE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_MODE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_PROFILE + ) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index d98709ba2b6..70477bbcdb2 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -64,6 +64,7 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = { UIWidget.ATLANTIC_ELECTRICAL_HEATER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) + UIWidget.ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.DOMESTIC_HOT_WATER_TANK: Platform.SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) UIWidget.MY_FOX_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index c17f30393fc..85e5a3fdf57 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -27,7 +27,10 @@ class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): """Initialize the device.""" super().__init__(coordinator) self.device_url = device_url - self.base_device_url, *_ = self.device_url.split("#") + split_device_url = self.device_url.split("#") + self.base_device_url = split_device_url[0] + if len(split_device_url) == 2: + self.index_device_url = split_device_url[1] self.executor = OverkizExecutor(device_url, coordinator) self._attr_assumed_state = not self.device.states