From cecdc3bd47f268a157cde5f63c42bf289f4f1faf Mon Sep 17 00:00:00 2001 From: Graham Brown Date: Tue, 20 Jun 2023 02:19:17 +0200 Subject: [PATCH] ESPHome Alarm Control Panel (#92357) --- .coveragerc | 1 + .../components/esphome/alarm_control_panel.py | 160 ++++++++++++++++++ .../components/esphome/entry_data.py | 2 + .../components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/esphome/alarm_control_panel.py diff --git a/.coveragerc b/.coveragerc index 242833b1262..944b2a5f838 100644 --- a/.coveragerc +++ b/.coveragerc @@ -305,6 +305,7 @@ omit = homeassistant/components/escea/climate.py homeassistant/components/escea/discovery.py homeassistant/components/esphome/__init__.py + homeassistant/components/esphome/alarm_control_panel.py homeassistant/components/esphome/binary_sensor.py homeassistant/components/esphome/bluetooth/* homeassistant/components/esphome/button.py diff --git a/homeassistant/components/esphome/alarm_control_panel.py b/homeassistant/components/esphome/alarm_control_panel.py new file mode 100644 index 00000000000..efa95dda710 --- /dev/null +++ b/homeassistant/components/esphome/alarm_control_panel.py @@ -0,0 +1,160 @@ +"""Support for ESPHome Alarm Control Panel.""" +from __future__ import annotations + +from aioesphomeapi import ( + AlarmControlPanelCommand, + AlarmControlPanelEntityState, + AlarmControlPanelInfo, + AlarmControlPanelState, + APIIntEnum, +) + +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanelEntity, + AlarmControlPanelEntityFeature, + CodeFormat, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, + STATE_ALARM_ARMING, + STATE_ALARM_DISARMED, + STATE_ALARM_DISARMING, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import EsphomeEntity, EsphomeEnumMapper, platform_async_setup_entry + +_ESPHOME_ACP_STATE_TO_HASS_STATE: EsphomeEnumMapper[ + AlarmControlPanelState, str +] = EsphomeEnumMapper( + { + AlarmControlPanelState.DISARMED: STATE_ALARM_DISARMED, + AlarmControlPanelState.ARMED_HOME: STATE_ALARM_ARMED_HOME, + AlarmControlPanelState.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + AlarmControlPanelState.ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, + AlarmControlPanelState.ARMED_VACATION: STATE_ALARM_ARMED_VACATION, + AlarmControlPanelState.ARMED_CUSTOM_BYPASS: STATE_ALARM_ARMED_CUSTOM_BYPASS, + AlarmControlPanelState.PENDING: STATE_ALARM_PENDING, + AlarmControlPanelState.ARMING: STATE_ALARM_ARMING, + AlarmControlPanelState.DISARMING: STATE_ALARM_DISARMING, + AlarmControlPanelState.TRIGGERED: STATE_ALARM_TRIGGERED, + } +) + + +class EspHomeACPFeatures(APIIntEnum): + """ESPHome AlarmCintolPanel feature numbers.""" + + ARM_HOME = 1 + ARM_AWAY = 2 + ARM_NIGHT = 4 + TRIGGER = 8 + ARM_CUSTOM_BYPASS = 16 + ARM_VACATION = 32 + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up ESPHome switches based on a config entry.""" + await platform_async_setup_entry( + hass, + entry, + async_add_entities, + component_key="alarm_control_panel", + info_type=AlarmControlPanelInfo, + entity_type=EsphomeAlarmControlPanel, + state_type=AlarmControlPanelEntityState, + ) + + +class EsphomeAlarmControlPanel( + EsphomeEntity[AlarmControlPanelInfo, AlarmControlPanelEntityState], + AlarmControlPanelEntity, +): + """An Alarm Control Panel implementation for ESPHome.""" + + @property + def state(self) -> str | None: + """Return the state of the device.""" + return _ESPHOME_ACP_STATE_TO_HASS_STATE.from_esphome(self._state.state) + + @property + def supported_features(self) -> AlarmControlPanelEntityFeature: + """Return the list of supported features.""" + feature = 0 + if self._static_info.supported_features & EspHomeACPFeatures.ARM_HOME: + feature |= AlarmControlPanelEntityFeature.ARM_HOME + if self._static_info.supported_features & EspHomeACPFeatures.ARM_AWAY: + feature |= AlarmControlPanelEntityFeature.ARM_AWAY + if self._static_info.supported_features & EspHomeACPFeatures.ARM_NIGHT: + feature |= AlarmControlPanelEntityFeature.ARM_NIGHT + if self._static_info.supported_features & EspHomeACPFeatures.TRIGGER: + feature |= AlarmControlPanelEntityFeature.TRIGGER + if self._static_info.supported_features & EspHomeACPFeatures.ARM_CUSTOM_BYPASS: + feature |= AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS + if self._static_info.supported_features & EspHomeACPFeatures.ARM_VACATION: + feature |= AlarmControlPanelEntityFeature.ARM_VACATION + return AlarmControlPanelEntityFeature(feature) + + @property + def code_format(self) -> CodeFormat | None: + """Return code format for disarm.""" + if self._static_info.requires_code: + return CodeFormat.NUMBER + return None + + @property + def code_arm_required(self) -> bool: + """Whether the code is required for arm actions.""" + return bool(self._static_info.requires_code_to_arm) + + async def async_alarm_disarm(self, code: str | None = None) -> None: + """Send disarm command.""" + await self._client.alarm_control_panel_command( + self._static_info.key, AlarmControlPanelCommand.DISARM, code + ) + + async def async_alarm_arm_home(self, code: str | None = None) -> None: + """Send arm home command.""" + await self._client.alarm_control_panel_command( + self._static_info.key, AlarmControlPanelCommand.ARM_HOME, code + ) + + async def async_alarm_arm_away(self, code: str | None = None) -> None: + """Send arm away command.""" + await self._client.alarm_control_panel_command( + self._static_info.key, AlarmControlPanelCommand.ARM_AWAY, code + ) + + async def async_alarm_arm_night(self, code: str | None = None) -> None: + """Send arm away command.""" + await self._client.alarm_control_panel_command( + self._static_info.key, AlarmControlPanelCommand.ARM_NIGHT, code + ) + + async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None: + """Send arm away command.""" + await self._client.alarm_control_panel_command( + self._static_info.key, AlarmControlPanelCommand.ARM_CUSTOM_BYPASS, code + ) + + async def async_alarm_arm_vacation(self, code: str | None = None) -> None: + """Send arm away command.""" + await self._client.alarm_control_panel_command( + self._static_info.key, AlarmControlPanelCommand.ARM_VACATION, code + ) + + async def async_alarm_trigger(self, code: str | None = None) -> None: + """Send alarm trigger command.""" + await self._client.alarm_control_panel_command( + self._static_info.key, AlarmControlPanelCommand.TRIGGER, code + ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 225ae3961e8..9c78e69709e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -9,6 +9,7 @@ from typing import Any, cast from aioesphomeapi import ( COMPONENT_TYPE_TO_INFO, + AlarmControlPanelInfo, APIClient, APIVersion, BinarySensorInfo, @@ -46,6 +47,7 @@ _LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = { + AlarmControlPanelInfo: Platform.ALARM_CONTROL_PANEL, BinarySensorInfo: Platform.BINARY_SENSOR, ButtonInfo: Platform.BUTTON, CameraInfo: Platform.CAMERA, diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index bc049153b8f..5a064e9b802 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -15,7 +15,7 @@ "iot_class": "local_push", "loggers": ["aioesphomeapi", "noiseprotocol"], "requirements": [ - "aioesphomeapi==14.0.0", + "aioesphomeapi==14.1.0", "bluetooth-data-tools==1.2.0", "esphome-dashboard-api==1.2.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 33f289e4425..aa907b6a834 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -234,7 +234,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==14.0.0 +aioesphomeapi==14.1.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e05e840f165..edc6f989b60 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aioecowitt==2023.5.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==14.0.0 +aioesphomeapi==14.1.0 # homeassistant.components.flo aioflo==2021.11.0