Add overkiz support for Atlantic Shogun ZoneControl 2.0 (AtlanticPassAPCHeatingAndCoolingZone) (#110510)
* Add Overkiz support for AtlanticPassAPCHeatingAndCoolingZone widget * Add support for AUTO HVAC mode for Atlantic Pass APC ZC devices that support it * Add support for multiple IO controllers for same widget (mainly for Atlantic APC) * Implement PR feedback * Small PR fixes * Fix constant inversion typo
This commit is contained in:
parent
fb10ef9ac0
commit
eeb87247e9
5 changed files with 348 additions and 4 deletions
|
@ -1,6 +1,8 @@
|
|||
"""Support for Overkiz climate devices."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@ -8,8 +10,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||
|
||||
from . import HomeAssistantOverkizData
|
||||
from .climate_entities import (
|
||||
WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY,
|
||||
WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY,
|
||||
WIDGET_TO_CLIMATE_ENTITY,
|
||||
Controllable,
|
||||
)
|
||||
from .const import DOMAIN
|
||||
|
||||
|
@ -28,6 +32,18 @@ async def async_setup_entry(
|
|||
if device.widget in WIDGET_TO_CLIMATE_ENTITY
|
||||
)
|
||||
|
||||
# Match devices based on the widget and controllableName
|
||||
# This is for example used for Atlantic APC, where devices with different functionality share the same uiClass and widget.
|
||||
async_add_entities(
|
||||
WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget][
|
||||
cast(Controllable, device.controllable_name)
|
||||
](device.device_url, data.coordinator)
|
||||
for device in data.platforms[Platform.CLIMATE]
|
||||
if device.widget in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY
|
||||
and device.controllable_name
|
||||
in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget]
|
||||
)
|
||||
|
||||
# Hitachi Air To Air Heat Pumps
|
||||
async_add_entities(
|
||||
WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY[device.widget][device.protocol](
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
"""Climate entities for the Overkiz (by Somfy) integration."""
|
||||
from enum import StrEnum, unique
|
||||
|
||||
from pyoverkiz.enums import Protocol
|
||||
from pyoverkiz.enums.ui import UIWidget
|
||||
|
||||
|
@ -10,18 +12,30 @@ from .atlantic_electrical_towel_dryer import AtlanticElectricalTowelDryer
|
|||
from .atlantic_heat_recovery_ventilation import AtlanticHeatRecoveryVentilation
|
||||
from .atlantic_pass_apc_heating_zone import AtlanticPassAPCHeatingZone
|
||||
from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl
|
||||
from .atlantic_pass_apc_zone_control_zone import AtlanticPassAPCZoneControlZone
|
||||
from .hitachi_air_to_air_heat_pump_hlrrwifi import HitachiAirToAirHeatPumpHLRRWIFI
|
||||
from .somfy_heating_temperature_interface import SomfyHeatingTemperatureInterface
|
||||
from .somfy_thermostat import SomfyThermostat
|
||||
from .valve_heating_temperature_interface import ValveHeatingTemperatureInterface
|
||||
|
||||
|
||||
@unique
|
||||
class Controllable(StrEnum):
|
||||
"""Enum for widget controllables."""
|
||||
|
||||
IO_ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE = (
|
||||
"io:AtlanticPassAPCHeatingAndCoolingZoneComponent"
|
||||
)
|
||||
IO_ATLANTIC_PASS_APC_ZONE_CONTROL_ZONE = (
|
||||
"io:AtlanticPassAPCZoneControlZoneComponent"
|
||||
)
|
||||
|
||||
|
||||
WIDGET_TO_CLIMATE_ENTITY = {
|
||||
UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater,
|
||||
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_ZONE: AtlanticPassAPCHeatingZone,
|
||||
UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: AtlanticPassAPCZoneControl,
|
||||
UIWidget.SOMFY_HEATING_TEMPERATURE_INTERFACE: SomfyHeatingTemperatureInterface,
|
||||
|
@ -29,6 +43,15 @@ WIDGET_TO_CLIMATE_ENTITY = {
|
|||
UIWidget.VALVE_HEATING_TEMPERATURE_INTERFACE: ValveHeatingTemperatureInterface,
|
||||
}
|
||||
|
||||
# For Atlantic APC, some devices are standalone and control themselves, some others needs to be
|
||||
# managed by a ZoneControl device. Widget name is the same in the two cases.
|
||||
WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY = {
|
||||
UIWidget.ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: {
|
||||
Controllable.IO_ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: AtlanticPassAPCHeatingZone,
|
||||
Controllable.IO_ATLANTIC_PASS_APC_ZONE_CONTROL_ZONE: AtlanticPassAPCZoneControlZone,
|
||||
}
|
||||
}
|
||||
|
||||
# Hitachi air-to-air heatpumps come in 2 flavors (HLRRWIFI and OVP) that are separated in 2 classes
|
||||
WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY = {
|
||||
UIWidget.HITACHI_AIR_TO_AIR_HEAT_PUMP: {
|
||||
|
|
|
@ -49,7 +49,15 @@ OVERKIZ_TO_PRESET_MODES: dict[str, str] = {
|
|||
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,
|
||||
|
|
|
@ -10,6 +10,7 @@ from homeassistant.components.climate import (
|
|||
)
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
|
||||
from ..coordinator import OverkizDataUpdateCoordinator
|
||||
from ..entity import OverkizEntity
|
||||
|
||||
OVERKIZ_TO_HVAC_MODE: dict[str, HVACMode] = {
|
||||
|
@ -25,16 +26,48 @@ HVAC_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODE.items()}
|
|||
class AtlanticPassAPCZoneControl(OverkizEntity, ClimateEntity):
|
||||
"""Representation of Atlantic Pass APC Zone Control."""
|
||||
|
||||
_attr_hvac_modes = [*HVAC_MODE_TO_OVERKIZ]
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
_enable_turn_on_off_backwards_compatibility = False
|
||||
|
||||
def __init__(
|
||||
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Init method."""
|
||||
super().__init__(device_url, coordinator)
|
||||
|
||||
self._attr_hvac_modes = [*HVAC_MODE_TO_OVERKIZ]
|
||||
|
||||
# Cooling is supported by a separate command
|
||||
if self.is_auto_hvac_mode_available:
|
||||
self._attr_hvac_modes.append(HVACMode.AUTO)
|
||||
|
||||
@property
|
||||
def is_auto_hvac_mode_available(self) -> bool:
|
||||
"""Check if auto mode is available on the ZoneControl."""
|
||||
|
||||
return self.executor.has_command(
|
||||
OverkizCommand.SET_HEATING_COOLING_AUTO_SWITCH
|
||||
) and self.executor.has_state(OverkizState.CORE_HEATING_COOLING_AUTO_SWITCH)
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return hvac operation ie. heat, cool mode."""
|
||||
|
||||
if (
|
||||
self.is_auto_hvac_mode_available
|
||||
and cast(
|
||||
str,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_HEATING_COOLING_AUTO_SWITCH
|
||||
),
|
||||
)
|
||||
== OverkizCommandParam.ON
|
||||
):
|
||||
return HVACMode.AUTO
|
||||
|
||||
return OVERKIZ_TO_HVAC_MODE[
|
||||
cast(
|
||||
str, self.executor.select_state(OverkizState.IO_PASS_APC_OPERATING_MODE)
|
||||
|
@ -43,6 +76,18 @@ class AtlanticPassAPCZoneControl(OverkizEntity, ClimateEntity):
|
|||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
|
||||
if self.is_auto_hvac_mode_available:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_HEATING_COOLING_AUTO_SWITCH,
|
||||
OverkizCommandParam.ON
|
||||
if hvac_mode == HVACMode.AUTO
|
||||
else OverkizCommandParam.OFF,
|
||||
)
|
||||
|
||||
if hvac_mode == HVACMode.AUTO:
|
||||
return
|
||||
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_PASS_APC_OPERATING_MODE, HVAC_MODE_TO_OVERKIZ[hvac_mode]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,252 @@
|
|||
"""Support for Atlantic Pass APC Heating Control."""
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import sleep
|
||||
from typing import Any, cast
|
||||
|
||||
from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
|
||||
|
||||
from homeassistant.components.climate import PRESET_NONE, HVACMode
|
||||
from homeassistant.const import ATTR_TEMPERATURE
|
||||
|
||||
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()}
|
||||
|
||||
TEMPERATURE_ZONECONTROL_DEVICE_INDEX = 1
|
||||
|
||||
|
||||
# Those device depends on a main probe that choose the operating mode (heating, cooling, ...)
|
||||
class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
|
||||
"""Representation of Atlantic Pass APC Heating And Cooling Zone Control."""
|
||||
|
||||
def __init__(
|
||||
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Init method."""
|
||||
super().__init__(device_url, coordinator)
|
||||
|
||||
# There is less supported functions, because they depend on the ZoneControl.
|
||||
if not self.is_using_derogated_temperature_fallback:
|
||||
# Modes are not configurable, they will follow current HVAC Mode of Zone Control.
|
||||
self._attr_hvac_modes = []
|
||||
|
||||
# Those are available and tested presets on Shogun.
|
||||
self._attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ]
|
||||
|
||||
# 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(
|
||||
TEMPERATURE_ZONECONTROL_DEVICE_INDEX
|
||||
)
|
||||
|
||||
@property
|
||||
def is_using_derogated_temperature_fallback(self) -> bool:
|
||||
"""Check if the device behave like the Pass APC Heating Zone."""
|
||||
|
||||
return self.executor.has_command(
|
||||
OverkizCommand.SET_DEROGATED_TARGET_TEMPERATURE
|
||||
)
|
||||
|
||||
@property
|
||||
def zone_control_hvac_mode(self) -> HVACMode:
|
||||
"""Return hvac operation ie. heat, cool, dry, off mode."""
|
||||
|
||||
if (
|
||||
state := self.zone_control_device.states[
|
||||
OverkizState.IO_PASS_APC_OPERATING_MODE
|
||||
]
|
||||
) is not None and (value := state.value_as_str) is not None:
|
||||
return OVERKIZ_TO_HVAC_MODE[value]
|
||||
return HVACMode.OFF
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return hvac operation ie. heat, cool, dry, off mode."""
|
||||
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return super().hvac_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."""
|
||||
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return await super().async_set_hvac_mode(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."""
|
||||
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return super().preset_mode
|
||||
|
||||
mode = OVERKIZ_MODE_TO_PRESET_MODES[
|
||||
cast(
|
||||
str,
|
||||
self.executor.select_state(
|
||||
OverkizState.IO_PASS_APC_COOLING_MODE
|
||||
if self.zone_control_hvac_mode == HVACMode.COOL
|
||||
else OverkizState.IO_PASS_APC_HEATING_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."""
|
||||
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return await super().async_set_preset_mode(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.is_using_derogated_temperature_fallback:
|
||||
return super().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
|
||||
),
|
||||
)
|
||||
|
||||
return cast(
|
||||
float, self.executor.select_state(OverkizState.CORE_TARGET_TEMPERATURE)
|
||||
)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new temperature."""
|
||||
|
||||
if self.is_using_derogated_temperature_fallback:
|
||||
return await super().async_set_temperature(**kwargs)
|
||||
|
||||
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."""
|
||||
|
||||
# The device needs a bit of time to update everything before a refresh.
|
||||
await sleep(2)
|
||||
|
||||
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
|
||||
)
|
Loading…
Add table
Reference in a new issue