Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add alarm control panel to Overkiz integration #67164

Merged
merged 9 commits into from
Mar 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
305 changes: 305 additions & 0 deletions homeassistant/components/overkiz/alarm_control_panel.py
Original file line number Diff line number Diff line change
@@ -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(
iMicknl marked this conversation as resolved.
Show resolved Hide resolved
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,
)
5 changes: 5 additions & 0 deletions homeassistant/components/overkiz/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
UPDATE_INTERVAL_ALL_ASSUMED_STATE: Final = timedelta(minutes=60)

PLATFORMS: list[Platform] = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Expand Down Expand Up @@ -58,15 +59,19 @@
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
Expand Down