Add support for Somfy Thermostat in Overkiz integration (#67169)
* Add Somfy Thermostat * Fix linked device Co-authored-by: Vincent Le Bourlot <vlebourl@gmail.com> * Mark Somfy thermostat as supported * Fix wrong usage of cast * Update presets to lowercase * Rename constants * Remove _saved_target_temp * Apply black * Clean code * Fix mypy errors * Use constants from pyoverkiz * Use enum for target temp * Add comment * Use ClimateEntityFeature * Ease code Co-authored-by: Mick Vleeshouwer <mick@imick.nl> * Remove unused imports * Use HVACAction * Use HVACMode * Use more Overkiz constants * Don’t copy/paste * Don’t use magic number Co-authored-by: Vincent Le Bourlot <vlebourl@gmail.com> Co-authored-by: Mick Vleeshouwer <mick@imick.nl>
This commit is contained in:
parent
2de4b193e3
commit
3571a80c8d
7 changed files with 182 additions and 3 deletions
|
@ -3,8 +3,10 @@ from pyoverkiz.enums.ui import UIWidget
|
|||
|
||||
from .atlantic_electrical_heater import AtlanticElectricalHeater
|
||||
from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl
|
||||
from .somfy_thermostat import SomfyThermostat
|
||||
|
||||
WIDGET_TO_CLIMATE_ENTITY = {
|
||||
UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater,
|
||||
UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: AtlanticPassAPCZoneControl,
|
||||
UIWidget.SOMFY_THERMOSTAT: SomfyThermostat,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
"""Support for Somfy Smart Thermostat."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
|
||||
|
||||
from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.components.climate.const import (
|
||||
PRESET_AWAY,
|
||||
PRESET_HOME,
|
||||
PRESET_NONE,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
||||
|
||||
from ..coordinator import OverkizDataUpdateCoordinator
|
||||
from ..entity import OverkizEntity
|
||||
|
||||
PRESET_FREEZE = "freeze"
|
||||
PRESET_NIGHT = "night"
|
||||
|
||||
STATE_DEROGATION_ACTIVE = "active"
|
||||
STATE_DEROGATION_INACTIVE = "inactive"
|
||||
|
||||
|
||||
OVERKIZ_TO_HVAC_MODES: dict[str, HVACMode] = {
|
||||
STATE_DEROGATION_ACTIVE: HVACMode.HEAT,
|
||||
STATE_DEROGATION_INACTIVE: HVACMode.AUTO,
|
||||
}
|
||||
HVAC_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODES.items()}
|
||||
|
||||
OVERKIZ_TO_PRESET_MODES: dict[OverkizCommandParam, str] = {
|
||||
OverkizCommandParam.AT_HOME_MODE: PRESET_HOME,
|
||||
OverkizCommandParam.AWAY_MODE: PRESET_AWAY,
|
||||
OverkizCommandParam.FREEZE_MODE: PRESET_FREEZE,
|
||||
OverkizCommandParam.MANUAL_MODE: PRESET_NONE,
|
||||
OverkizCommandParam.SLEEPING_MODE: PRESET_NIGHT,
|
||||
OverkizCommandParam.SUDDEN_DROP_MODE: PRESET_NONE,
|
||||
}
|
||||
PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODES.items()}
|
||||
TARGET_TEMP_TO_OVERKIZ = {
|
||||
PRESET_HOME: OverkizState.SOMFY_THERMOSTAT_AT_HOME_TARGET_TEMPERATURE,
|
||||
PRESET_AWAY: OverkizState.SOMFY_THERMOSTAT_AWAY_MODE_TARGET_TEMPERATURE,
|
||||
PRESET_FREEZE: OverkizState.SOMFY_THERMOSTAT_FREEZE_MODE_TARGET_TEMPERATURE,
|
||||
PRESET_NIGHT: OverkizState.SOMFY_THERMOSTAT_SLEEPING_MODE_TARGET_TEMPERATURE,
|
||||
}
|
||||
|
||||
# controllableName is somfythermostat:SomfyThermostatTemperatureSensor
|
||||
TEMPERATURE_SENSOR_DEVICE_INDEX = 2
|
||||
|
||||
|
||||
class SomfyThermostat(OverkizEntity, ClimateEntity):
|
||||
"""Representation of Somfy Smart Thermostat."""
|
||||
|
||||
_attr_temperature_unit = TEMP_CELSIUS
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
)
|
||||
_attr_hvac_modes = [*HVAC_MODES_TO_OVERKIZ]
|
||||
_attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ]
|
||||
# Both min and max temp values have been retrieved from the Somfy Application.
|
||||
_attr_min_temp = 15.0
|
||||
_attr_max_temp = 26.0
|
||||
|
||||
def __init__(
|
||||
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Init method."""
|
||||
super().__init__(device_url, coordinator)
|
||||
self.temperature_device = self.executor.linked_device(
|
||||
TEMPERATURE_SENSOR_DEVICE_INDEX
|
||||
)
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> str:
|
||||
"""Return hvac operation ie. heat, cool mode."""
|
||||
return OVERKIZ_TO_HVAC_MODES[
|
||||
cast(
|
||||
str, self.executor.select_state(OverkizState.CORE_DEROGATION_ACTIVATION)
|
||||
)
|
||||
]
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> str:
|
||||
"""Return the current running hvac operation if supported."""
|
||||
if not self.current_temperature or not self.target_temperature:
|
||||
return HVACAction.IDLE
|
||||
if self.current_temperature < self.target_temperature:
|
||||
return HVACAction.HEATING
|
||||
return HVACAction.IDLE
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str:
|
||||
"""Return the current preset mode, e.g., home, away, temp."""
|
||||
if self.hvac_mode == HVACMode.AUTO:
|
||||
state_key = OverkizState.SOMFY_THERMOSTAT_HEATING_MODE
|
||||
else:
|
||||
state_key = OverkizState.SOMFY_THERMOSTAT_DEROGATION_HEATING_MODE
|
||||
|
||||
state = cast(str, self.executor.select_state(state_key))
|
||||
|
||||
return OVERKIZ_TO_PRESET_MODES[OverkizCommandParam(state)]
|
||||
|
||||
@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 target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.hvac_mode == HVACMode.AUTO:
|
||||
if self.preset_mode == PRESET_NONE:
|
||||
return None
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(TARGET_TEMP_TO_OVERKIZ[self.preset_mode]),
|
||||
)
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(OverkizState.CORE_DEROGATED_TARGET_TEMPERATURE),
|
||||
)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs[ATTR_TEMPERATURE]
|
||||
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_DEROGATION,
|
||||
temperature,
|
||||
OverkizCommandParam.FURTHER_NOTICE,
|
||||
)
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_MODE_TEMPERATURE,
|
||||
OverkizCommandParam.MANUAL_MODE,
|
||||
temperature,
|
||||
)
|
||||
await self.executor.async_execute_command(OverkizCommand.REFRESH_STATE)
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
if hvac_mode == HVACMode.AUTO:
|
||||
await self.executor.async_execute_command(OverkizCommand.EXIT_DEROGATION)
|
||||
await self.executor.async_execute_command(OverkizCommand.REFRESH_STATE)
|
||||
else:
|
||||
await self.async_set_preset_mode(PRESET_NONE)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
if preset_mode in [PRESET_FREEZE, PRESET_NIGHT, PRESET_AWAY, PRESET_HOME]:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_DEROGATION,
|
||||
PRESET_MODES_TO_OVERKIZ[preset_mode],
|
||||
OverkizCommandParam.FURTHER_NOTICE,
|
||||
)
|
||||
elif preset_mode == PRESET_NONE:
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_DEROGATION,
|
||||
self.target_temperature,
|
||||
OverkizCommandParam.FURTHER_NOTICE,
|
||||
)
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_MODE_TEMPERATURE,
|
||||
OverkizCommandParam.MANUAL_MODE,
|
||||
self.target_temperature,
|
||||
)
|
||||
await self.executor.async_execute_command(OverkizCommand.REFRESH_STATE)
|
|
@ -70,6 +70,7 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = {
|
|||
UIWidget.RTD_OUTDOOR_SIREN: Platform.SWITCH, # widgetName, uiClass is Siren (not supported)
|
||||
UIWidget.RTS_GENERIC: Platform.COVER, # widgetName, uiClass is Generic (not supported)
|
||||
UIWidget.SIREN_STATUS: None, # widgetName, uiClass is Siren (siren)
|
||||
UIWidget.SOMFY_THERMOSTAT: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported)
|
||||
UIWidget.STATELESS_ALARM_CONTROLLER: Platform.SWITCH, # widgetName, uiClass is Alarm (not supported)
|
||||
UIWidget.STATEFUL_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported)
|
||||
UIWidget.STATELESS_EXTERIOR_HEATING: Platform.SWITCH, # widgetName, uiClass is ExteriorHeatingSystem (not supported)
|
||||
|
|
|
@ -37,6 +37,10 @@ class OverkizExecutor:
|
|||
"""Return Overkiz device linked to this entity."""
|
||||
return self.coordinator.data[self.device_url]
|
||||
|
||||
def linked_device(self, index: int) -> Device:
|
||||
"""Return Overkiz device sharing the same base url."""
|
||||
return self.coordinator.data[f"{self.base_device_url}#{index}"]
|
||||
|
||||
def select_command(self, *commands: str) -> str | None:
|
||||
"""Select first existing command in a list of commands."""
|
||||
existing_commands = self.device.definition.commands
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Overkiz (by Somfy)",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/overkiz",
|
||||
"requirements": ["pyoverkiz==1.4.0"],
|
||||
"requirements": ["pyoverkiz==1.4.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_kizbox._tcp.local.",
|
||||
|
|
|
@ -1720,7 +1720,7 @@ pyotgw==1.1b1
|
|||
pyotp==2.6.0
|
||||
|
||||
# homeassistant.components.overkiz
|
||||
pyoverkiz==1.4.0
|
||||
pyoverkiz==1.4.1
|
||||
|
||||
# homeassistant.components.openweathermap
|
||||
pyowm==3.2.0
|
||||
|
|
|
@ -1169,7 +1169,7 @@ pyotgw==1.1b1
|
|||
pyotp==2.6.0
|
||||
|
||||
# homeassistant.components.overkiz
|
||||
pyoverkiz==1.4.0
|
||||
pyoverkiz==1.4.1
|
||||
|
||||
# homeassistant.components.openweathermap
|
||||
pyowm==3.2.0
|
||||
|
|
Loading…
Add table
Reference in a new issue