From d0e5e51863d7649c19bab67f87b0d147d030243c Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 28 Mar 2022 15:19:16 -0700 Subject: [PATCH] Add alarm control panel to Overkiz integration (#67164) --- .coveragerc | 1 + .../components/overkiz/alarm_control_panel.py | 305 ++++++++++++++++++ homeassistant/components/overkiz/const.py | 5 + 3 files changed, 311 insertions(+) create mode 100644 homeassistant/components/overkiz/alarm_control_panel.py diff --git a/.coveragerc b/.coveragerc index 254515c3745..3579aad4b48 100644 --- a/.coveragerc +++ b/.coveragerc @@ -861,6 +861,7 @@ omit = homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py homeassistant/components/overkiz/__init__.py + homeassistant/components/overkiz/alarm_control_panel.py homeassistant/components/overkiz/binary_sensor.py homeassistant/components/overkiz/button.py homeassistant/components/overkiz/climate.py diff --git a/homeassistant/components/overkiz/alarm_control_panel.py b/homeassistant/components/overkiz/alarm_control_panel.py new file mode 100644 index 00000000000..f6bdb6eef0e --- /dev/null +++ b/homeassistant/components/overkiz/alarm_control_panel.py @@ -0,0 +1,305 @@ +"""Support for Overkiz alarm control panel.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState +from pyoverkiz.enums.ui import UIWidget +from pyoverkiz.types import StateType as OverkizStateType + +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanelEntity, + AlarmControlPanelEntityDescription, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeAssistantOverkizData +from .const import DOMAIN +from .coordinator import OverkizDataUpdateCoordinator +from .entity import OverkizDescriptiveEntity + + +@dataclass +class OverkizAlarmDescriptionMixin: + """Define an entity description mixin for switch entities.""" + + supported_features: int + fn_state: Callable[[Callable[[str], OverkizStateType]], str] + + +@dataclass +class OverkizAlarmDescription( + AlarmControlPanelEntityDescription, OverkizAlarmDescriptionMixin +): + """Class to describe an Overkiz alarm control panel.""" + + alarm_disarm: str | None = None + alarm_disarm_args: OverkizStateType | list[OverkizStateType] | None = None + alarm_arm_home: str | None = None + alarm_arm_home_args: OverkizStateType | list[OverkizStateType] | None = None + alarm_arm_night: str | None = None + alarm_arm_night_args: OverkizStateType | list[OverkizStateType] | None = None + alarm_arm_away: str | None = None + alarm_arm_away_args: OverkizStateType | list[OverkizStateType] | None = None + alarm_trigger: str | None = None + alarm_trigger_args: OverkizStateType | list[OverkizStateType] | None = None + + +MAP_INTERNAL_STATUS_STATE: dict[str, str] = { + OverkizCommandParam.OFF: STATE_ALARM_DISARMED, + OverkizCommandParam.ZONE_1: STATE_ALARM_ARMED_HOME, + OverkizCommandParam.ZONE_2: STATE_ALARM_ARMED_NIGHT, + OverkizCommandParam.TOTAL: STATE_ALARM_ARMED_AWAY, +} + + +def _state_tsk_alarm_controller(select_state: Callable[[str], OverkizStateType]) -> str: + """Return the state of the device.""" + if ( + cast(str, select_state(OverkizState.INTERNAL_INTRUSION_DETECTED)) + == OverkizCommandParam.DETECTED + ): + return STATE_ALARM_TRIGGERED + + if cast(str, select_state(OverkizState.INTERNAL_CURRENT_ALARM_MODE)) != cast( + str, select_state(OverkizState.INTERNAL_TARGET_ALARM_MODE) + ): + return STATE_ALARM_PENDING + + return MAP_INTERNAL_STATUS_STATE[ + cast(str, select_state(OverkizState.INTERNAL_TARGET_ALARM_MODE)) + ] + + +def _state_stateful_alarm_controller( + select_state: Callable[[str], OverkizStateType] +) -> str: + """Return the state of the device.""" + if state := cast(list, select_state(OverkizState.CORE_ACTIVE_ZONES)): + if [ + OverkizCommandParam.A, + OverkizCommandParam.B, + OverkizCommandParam.C, + ] in state: + return STATE_ALARM_ARMED_AWAY + + if [OverkizCommandParam.A, OverkizCommandParam.B] in state: + return STATE_ALARM_ARMED_NIGHT + + if OverkizCommandParam.A in state: + return STATE_ALARM_ARMED_HOME + + return STATE_ALARM_DISARMED + + +MAP_MYFOX_STATUS_STATE: dict[str, str] = { + OverkizCommandParam.ARMED: STATE_ALARM_ARMED_AWAY, + OverkizCommandParam.DISARMED: STATE_ALARM_DISARMED, + OverkizCommandParam.PARTIAL: STATE_ALARM_ARMED_NIGHT, +} + + +def _state_myfox_alarm_controller( + select_state: Callable[[str], OverkizStateType] +) -> str: + """Return the state of the device.""" + if ( + cast(str, select_state(OverkizState.CORE_INTRUSION)) + == OverkizCommandParam.DETECTED + ): + return STATE_ALARM_TRIGGERED + + return MAP_MYFOX_STATUS_STATE[ + cast(str, select_state(OverkizState.MYFOX_ALARM_STATUS)) + ] + + +MAP_ARM_TYPE: dict[str, str] = { + OverkizCommandParam.DISARMED: STATE_ALARM_DISARMED, + OverkizCommandParam.ARMED_DAY: STATE_ALARM_ARMED_HOME, + OverkizCommandParam.ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, + OverkizCommandParam.ARMED: STATE_ALARM_ARMED_AWAY, +} + + +def _state_alarm_panel_controller( + select_state: Callable[[str], OverkizStateType] +) -> str: + """Return the state of the device.""" + return MAP_ARM_TYPE[ + cast(str, select_state(OverkizState.VERISURE_ALARM_PANEL_MAIN_ARM_TYPE)) + ] + + +ALARM_DESCRIPTIONS: list[OverkizAlarmDescription] = [ + # TSKAlarmController + # Disabled by default since all Overkiz hubs have this + # virtual device, but only a few users actually use this. + OverkizAlarmDescription( + key=UIWidget.TSKALARM_CONTROLLER, + entity_registry_enabled_default=False, + supported_features=( + SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ), + fn_state=_state_tsk_alarm_controller, + alarm_disarm=OverkizCommand.ALARM_OFF, + alarm_arm_home=OverkizCommand.SET_TARGET_ALARM_MODE, + alarm_arm_home_args=OverkizCommandParam.PARTIAL_1, + alarm_arm_night=OverkizCommand.SET_TARGET_ALARM_MODE, + alarm_arm_night_args=OverkizCommandParam.PARTIAL_2, + alarm_arm_away=OverkizCommand.SET_TARGET_ALARM_MODE, + alarm_arm_away_args=OverkizCommandParam.TOTAL, + alarm_trigger=OverkizCommand.ALARM_ON, + ), + # StatefulAlarmController + OverkizAlarmDescription( + key=UIWidget.STATEFUL_ALARM_CONTROLLER, + supported_features=( + SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT + ), + fn_state=_state_stateful_alarm_controller, + alarm_disarm=OverkizCommand.DISARM, + alarm_arm_home=OverkizCommand.ALARM_ZONE_ON, + alarm_arm_home_args=[OverkizCommandParam.A], + alarm_arm_night=OverkizCommand.ALARM_ZONE_ON, + alarm_arm_night_args=[OverkizCommandParam.A, OverkizCommandParam.B], + alarm_arm_away=OverkizCommand.ALARM_ZONE_ON, + alarm_arm_away_args=[ + OverkizCommandParam.A, + OverkizCommandParam.B, + OverkizCommandParam.C, + ], + ), + # MyFoxAlarmController + OverkizAlarmDescription( + key=UIWidget.MY_FOX_ALARM_CONTROLLER, + supported_features=SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT, + fn_state=_state_myfox_alarm_controller, + alarm_disarm=OverkizCommand.DISARM, + alarm_arm_night=OverkizCommand.PARTIAL, + alarm_arm_away=OverkizCommand.ARM, + ), + # AlarmPanelController + OverkizAlarmDescription( + key=UIWidget.ALARM_PANEL_CONTROLLER, + supported_features=( + SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_NIGHT + ), + fn_state=_state_alarm_panel_controller, + alarm_disarm=OverkizCommand.DISARM, + alarm_arm_home=OverkizCommand.ARM_PARTIAL_DAY, + alarm_arm_night=OverkizCommand.ARM_PARTIAL_NIGHT, + alarm_arm_away=OverkizCommand.ARM, + ), +] + +SUPPORTED_DEVICES = {description.key: description for description in ALARM_DESCRIPTIONS} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Overkiz alarm control panel from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + entities: list[OverkizAlarmControlPanel] = [] + + for device in data.platforms[Platform.ALARM_CONTROL_PANEL]: + if description := SUPPORTED_DEVICES.get(device.widget) or SUPPORTED_DEVICES.get( + device.ui_class + ): + entities.append( + OverkizAlarmControlPanel( + device.device_url, + data.coordinator, + description, + ) + ) + + async_add_entities(entities) + + +class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity): + """Representation of an Overkiz Alarm Control Panel.""" + + entity_description: OverkizAlarmDescription + + def __init__( + self, + device_url: str, + coordinator: OverkizDataUpdateCoordinator, + description: EntityDescription, + ) -> None: + """Initialize the device.""" + super().__init__(device_url, coordinator, description) + + self._attr_supported_features = self.entity_description.supported_features + + @property + def state(self) -> str: + """Return the state of the device.""" + return self.entity_description.fn_state(self.executor.select_state) + + async def async_alarm_disarm(self, code: str | None = None) -> None: + """Send disarm command.""" + assert self.entity_description.alarm_disarm + await self.executor.async_execute_command( + self.entity_description.alarm_disarm, + self.entity_description.alarm_disarm_args, + ) + + async def async_alarm_arm_home(self, code: str | None = None) -> None: + """Send arm home command.""" + assert self.entity_description.alarm_arm_home + await self.executor.async_execute_command( + self.entity_description.alarm_arm_home, + self.entity_description.alarm_arm_home_args, + ) + + async def async_alarm_arm_night(self, code: str | None = None) -> None: + """Send arm night command.""" + assert self.entity_description.alarm_arm_night + await self.executor.async_execute_command( + self.entity_description.alarm_arm_night, + self.entity_description.alarm_arm_night_args, + ) + + async def async_alarm_arm_away(self, code: str | None = None) -> None: + """Send arm away command.""" + assert self.entity_description.alarm_arm_away + await self.executor.async_execute_command( + self.entity_description.alarm_arm_away, + self.entity_description.alarm_arm_away_args, + ) + + async def async_alarm_trigger(self, code: str | None = None) -> None: + """Send alarm trigger command.""" + assert self.entity_description.alarm_trigger + await self.executor.async_execute_command( + self.entity_description.alarm_trigger, + self.entity_description.alarm_trigger_args, + ) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 7f031ef3b6a..119f7a32262 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -19,6 +19,7 @@ UPDATE_INTERVAL: Final = timedelta(seconds=30) UPDATE_INTERVAL_ALL_ASSUMED_STATE: Final = timedelta(minutes=60) PLATFORMS: list[Platform] = [ + Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, @@ -58,15 +59,19 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = { UIClass.SWINGING_SHUTTER: Platform.COVER, UIClass.VENETIAN_BLIND: Platform.COVER, UIClass.WINDOW: Platform.COVER, + UIWidget.ALARM_PANEL_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.ATLANTIC_ELECTRICAL_HEATER: 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) UIWidget.MY_FOX_SECURITY_CAMERA: Platform.SWITCH, # widgetName, uiClass is Camera (not supported) UIWidget.RTD_INDOOR_SIREN: Platform.SWITCH, # widgetName, uiClass is Siren (not supported) 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.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) + UIWidget.TSKALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) } # Map Overkiz camelCase to Home Assistant snake_case for translation