From 5bec218ac236a749950774a7c91463b36201c616 Mon Sep 17 00:00:00 2001 From: Jeremy Trufier Date: Tue, 13 Feb 2024 12:16:48 +0100 Subject: [PATCH] Add Overkiz support for AtlanticPassAPCHeatingAndCoolingZone widget --- CODEOWNERS | 4 +- .../overkiz/climate_entities/__init__.py | 6 +- ...antic_pass_apc_heating_and_cooling_zone.py | 228 ++++++++++++++++++ .../atlantic_pass_apc_heating_zone.py | 10 +- .../atlantic_pass_apc_zone_control.py | 1 + .../components/overkiz/manifest.json | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py diff --git a/CODEOWNERS b/CODEOWNERS index 7e53ae3058b8be..9c5e199f069536 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -969,8 +969,8 @@ build.json @home-assistant/supervisor /tests/components/otbr/ @home-assistant/core /homeassistant/components/ourgroceries/ @OnFreund /tests/components/ourgroceries/ @OnFreund -/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev -/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev +/homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 +/tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev @tronix117 /homeassistant/components/ovo_energy/ @timmo001 /tests/components/ovo_energy/ @timmo001 /homeassistant/components/p1_monitor/ @klaasnicolaas diff --git a/homeassistant/components/overkiz/climate_entities/__init__.py b/homeassistant/components/overkiz/climate_entities/__init__.py index c74ff2829cccc5..a6d0dcb52c167f 100644 --- a/homeassistant/components/overkiz/climate_entities/__init__.py +++ b/homeassistant/components/overkiz/climate_entities/__init__.py @@ -8,6 +8,9 @@ ) 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_heating_zone import AtlanticPassAPCHeatingZone from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl from .hitachi_air_to_air_heat_pump_hlrrwifi import HitachiAirToAirHeatPumpHLRRWIFI @@ -20,8 +23,7 @@ UIWidget.ATLANTIC_ELECTRICAL_HEATER_WITH_ADJUSTABLE_TEMPERATURE_SETPOINT: AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint, UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: AtlanticElectricalTowelDryer, UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: AtlanticHeatRecoveryVentilation, - # ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE works exactly the same as ATLANTIC_PASS_APC_HEATING_ZONE - UIWidget.ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: AtlanticPassAPCHeatingZone, + UIWidget.ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: AtlanticPassAPCHeatingAndCoolingZone, UIWidget.ATLANTIC_PASS_APC_HEATING_ZONE: AtlanticPassAPCHeatingZone, UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: AtlanticPassAPCZoneControl, UIWidget.SOMFY_HEATING_TEMPERATURE_INTERFACE: SomfyHeatingTemperatureInterface, 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 00000000000000..d0c2a98cbc2b4b --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py @@ -0,0 +1,228 @@ +"""Support for Atlantic Pass APC Heating Control.""" +from __future__ import annotations + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.climate import ClimateEntityFeature, HVACMode +from homeassistant.components.climate.const import PRESET_NONE +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature + +from ..const import DOMAIN +from ..coordinator import OverkizDataUpdateCoordinator +from .atlantic_pass_apc_heating_zone import AtlanticPassAPCHeatingZone +from .atlantic_pass_apc_zone_control import OVERKIZ_TO_HVAC_MODE + +PRESET_SCHEDULE = "schedule" +PRESET_MANUAL = "manual" + +OVERKIZ_MODE_TO_PRESET_MODES: dict[str, str] = { + OverkizCommandParam.MANU: PRESET_MANUAL, + OverkizCommandParam.INTERNAL_SCHEDULING: PRESET_SCHEDULE, +} + +PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_MODE_TO_PRESET_MODES.items()} + +# Those device depends on a main probe that choose the operating mode (heating, cooling, ...) +class AtlanticPassAPCHeatingAndCoolingZone(AtlanticPassAPCHeatingZone): + """Representation of Atlantic Pass APC Heating And Cooling Zone Control.""" + + # Modes are not configurable, they will follow current HVAC Mode of Zone Control. + _attr_hvac_modes = [] + + # Those are available and tested presets on Shogun. + _attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] + + # Thermostat internalScheduling stop manu eco + + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON + ) + _attr_temperature_unit = UnitOfTemperature.CELSIUS + _attr_translation_key = DOMAIN + _enable_turn_on_off_backwards_compatibility = False + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + + # Those APC Heating and Cooling probes depends on the zone control device (main probe). + # Only the base device (#1) can be used to get/set some states. + # Like to retrieve and set the current operating mode (heating, cooling, drying, off). + self.zone_control_device = self.executor.linked_device(1) + + @property + def zone_control_hvac_mode(self) -> HVACMode: + """Return hvac operation ie. heat, cool, dry, off mode.""" + + return OVERKIZ_TO_HVAC_MODE[ + self.zone_control_device.states[ + OverkizState.IO_PASS_APC_OPERATING_MODE + ].value_as_str + ] + + @property + def hvac_mode(self) -> HVACMode: + """Return hvac operation ie. heat, cool, dry, off mode.""" + + zone_control_hvac_mode = self.zone_control_hvac_mode + + # Should be same, because either thermostat or this integration change both. + on_off_state = cast( + str, + self.executor.select_state( + OverkizState.CORE_COOLING_ON_OFF + if zone_control_hvac_mode == HVACMode.COOL + else OverkizState.CORE_HEATING_ON_OFF + ), + ) + + # Device is Stopped, it means the air flux is flowing but its venting door is closed. + if on_off_state == OverkizCommandParam.OFF: + hvac_mode = HVACMode.OFF + else: + hvac_mode = zone_control_hvac_mode + + # It helps keep it consistent with the Zone Control, within the interface. + if self._attr_hvac_modes != [zone_control_hvac_mode, HVACMode.OFF]: + self._attr_hvac_modes = [zone_control_hvac_mode, HVACMode.OFF] + self.async_write_ha_state() + + return hvac_mode + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + + # They are mainly managed by the Zone Control device + # However, it make sense to map the OFF Mode to the Overkiz STOP Preset + + if hvac_mode == HVACMode.OFF: + await self.executor.async_execute_command( + OverkizCommand.SET_COOLING_ON_OFF, + OverkizCommandParam.OFF, + ) + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_ON_OFF, + OverkizCommandParam.OFF, + ) + else: + await self.executor.async_execute_command( + OverkizCommand.SET_COOLING_ON_OFF, + OverkizCommandParam.ON, + ) + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_ON_OFF, + OverkizCommandParam.ON, + ) + + await self.async_refresh_modes() + + @property + def preset_mode(self) -> str: + """Return the current preset mode, e.g., schedule, manual.""" + + mode = OVERKIZ_MODE_TO_PRESET_MODES[ + cast( + str, + self.executor.select_state( + OverkizState.IO_PASS_APC_HEATING_MODE + if self.zone_control_hvac_mode == HVACMode.COOL + else OverkizState.IO_PASS_APC_COOLING_MODE + ), + ) + ] + + return mode if mode is not None else PRESET_NONE + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + + mode = PRESET_MODES_TO_OVERKIZ[preset_mode] + + # For consistency, it is better both are synced like on the Thermostat. + await self.executor.async_execute_command( + OverkizCommand.SET_PASS_APC_HEATING_MODE, mode + ) + await self.executor.async_execute_command( + OverkizCommand.SET_PASS_APC_COOLING_MODE, mode + ) + + await self.async_refresh_modes() + + @property + def target_temperature(self) -> float: + """Return hvac target temperature.""" + + if self.zone_control_hvac_mode == HVACMode.COOL: + return cast( + float, + self.executor.select_state( + OverkizState.CORE_COOLING_TARGET_TEMPERATURE + ), + ) + + if self.zone_control_hvac_mode == HVACMode.HEAT: + return cast( + float, + self.executor.select_state( + OverkizState.CORE_HEATING_TARGET_TEMPERATURE + ), + ) + + # Not sure this temperature is relevant. + 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] + + # Change both (heating/cooling) temperature is a good way to have consistency + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_TARGET_TEMPERATURE, + temperature, + ) + await self.executor.async_execute_command( + OverkizCommand.SET_COOLING_TARGET_TEMPERATURE, + temperature, + ) + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATION_ON_OFF_STATE, + OverkizCommandParam.OFF, + ) + + # Target temperature may take up to 1 minute to get refreshed. + await self.executor.async_execute_command( + OverkizCommand.REFRESH_TARGET_TEMPERATURE + ) + + async def async_refresh_modes(self) -> None: + """Refresh the device modes to have new states.""" + + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_MODE + ) + + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_PROFILE + ) + + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_COOLING_MODE + ) + + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_COOLING_PROFILE + ) + + await self.executor.async_execute_command( + OverkizCommand.REFRESH_TARGET_TEMPERATURE + ) diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py index 25dab7c1d7e0db..157ec72a2496ef 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py @@ -49,7 +49,15 @@ OverkizCommandParam.INTERNAL_SCHEDULING: PRESET_HOME, } -PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODES.items()} +PRESET_MODES_TO_OVERKIZ: dict[str, str] = { + PRESET_COMFORT: OverkizCommandParam.COMFORT, + PRESET_AWAY: OverkizCommandParam.ABSENCE, + PRESET_ECO: OverkizCommandParam.ECO, + PRESET_FROST_PROTECTION: OverkizCommandParam.FROSTPROTECTION, + PRESET_EXTERNAL: OverkizCommandParam.EXTERNAL_SCHEDULING, + PRESET_HOME: OverkizCommandParam.INTERNAL_SCHEDULING, +} + OVERKIZ_TO_PROFILE_MODES: dict[str, str] = { OverkizCommandParam.OFF: PRESET_SLEEP, diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py index fe9f20b05fc02c..66486458ae799c 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py @@ -43,6 +43,7 @@ def hvac_mode(self) -> HVACMode: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" + await self.executor.async_execute_command( OverkizCommand.SET_PASS_APC_OPERATING_MODE, HVAC_MODE_TO_OVERKIZ[hvac_mode] ) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index e5c1665b2e48c4..de3941a2a76897 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -1,7 +1,7 @@ { "domain": "overkiz", "name": "Overkiz", - "codeowners": ["@imicknl", "@vlebourl", "@tetienne", "@nyroDev"], + "codeowners": ["@imicknl", "@vlebourl", "@tetienne", "@nyroDev", "@tronix117"], "config_flow": true, "dhcp": [ { @@ -13,7 +13,7 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz==1.13.3"], + "requirements": ["pyoverkiz==1.13.6"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 8c4a57aaa5bee7..1d5cd9875e0275 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2023,7 +2023,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.3 +pyoverkiz==1.13.6 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a9e7576908915..9d6b7e508b3514 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1562,7 +1562,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.3 +pyoverkiz==1.13.6 # homeassistant.components.openweathermap pyowm==3.2.0