diff --git a/.coveragerc b/.coveragerc index 48e4561e54e..d0b4f2fe3f8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -295,9 +295,11 @@ omit = homeassistant/components/elkm1/sensor.py homeassistant/components/elkm1/switch.py homeassistant/components/elmax/__init__.py + homeassistant/components/elmax/alarm_control_panel.py homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/common.py homeassistant/components/elmax/const.py + homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/switch.py homeassistant/components/elv/* homeassistant/components/emby/media_player.py diff --git a/homeassistant/components/elmax/alarm_control_panel.py b/homeassistant/components/elmax/alarm_control_panel.py new file mode 100644 index 00000000000..33b8749cb48 --- /dev/null +++ b/homeassistant/components/elmax/alarm_control_panel.py @@ -0,0 +1,108 @@ +"""Elmax sensor platform.""" +from __future__ import annotations + +from elmax_api.model.alarm_status import AlarmArmStatus, AlarmStatus +from elmax_api.model.command import AreaCommand +from elmax_api.model.panel import PanelStatus + +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_DISARMED +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import InvalidStateError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ElmaxCoordinator +from .common import ElmaxEntity +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Elmax area platform.""" + coordinator: ElmaxCoordinator = hass.data[DOMAIN][config_entry.entry_id] + known_devices = set() + + def _discover_new_devices(): + panel_status: PanelStatus = coordinator.data + # In case the panel is offline, its status will be None. In that case, simply do nothing + if panel_status is None: + return + + # Otherwise, add all the entities we found + entities = [ + ElmaxArea( + panel=coordinator.panel_entry, + elmax_device=area, + panel_version=panel_status.release, + coordinator=coordinator, + ) + for area in panel_status.areas + if area.endpoint_id not in known_devices + ] + + if entities: + async_add_entities(entities) + known_devices.update([e.unique_id for e in entities]) + + # Register a listener for the discovery of new devices + config_entry.async_on_unload(coordinator.async_add_listener(_discover_new_devices)) + + # Immediately run a discovery, so we don't need to wait for the next update + _discover_new_devices() + + +class ElmaxArea(ElmaxEntity, AlarmControlPanelEntity): + """Elmax Area entity implementation.""" + + _attr_code_format = CodeFormat.NUMBER + _attr_code_arm_required = False + _attr_has_entity_name = True + _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY + + async def async_alarm_arm_away(self, code: str | None = None) -> None: + """Send arm away command.""" + if self._attr_state == AlarmStatus.NOT_ARMED_NOT_ARMABLE: + raise InvalidStateError( + f"Cannot arm {self.name}: please check for open windows/doors first" + ) + + await self.coordinator.http_client.execute_command( + endpoint_id=self._device.endpoint_id, + command=AreaCommand.ARM_TOTALLY, + extra_payload={"code": code}, + ) + await self.coordinator.async_refresh() + + async def async_alarm_disarm(self, code: str | None = None) -> None: + """Send disarm command.""" + await self.coordinator.http_client.execute_command( + endpoint_id=self._device.endpoint_id, + command=AreaCommand.DISARM, + extra_payload={"code": code}, + ) + await self.coordinator.async_refresh() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_state = ALARM_STATE_TO_HA.get( + self.coordinator.get_area_state(self._device.endpoint_id).armed_status + ) + super()._handle_coordinator_update() + + +ALARM_STATE_TO_HA = { + AlarmArmStatus.ARMED_TOTALLY: STATE_ALARM_ARMED_AWAY, + AlarmArmStatus.ARMED_P1_P2: STATE_ALARM_ARMED_AWAY, + AlarmArmStatus.ARMED_P2: STATE_ALARM_ARMED_AWAY, + AlarmArmStatus.ARMED_P1: STATE_ALARM_ARMED_AWAY, + AlarmArmStatus.NOT_ARMED: STATE_ALARM_DISARMED, +} diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index 4116ff05f44..80738478a49 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -14,6 +14,7 @@ from elmax_api.exceptions import ( ) from elmax_api.http import Elmax from elmax_api.model.actuator import Actuator +from elmax_api.model.area import Area from elmax_api.model.endpoint import DeviceEndpoint from elmax_api.model.panel import PanelEntry, PanelStatus @@ -62,15 +63,21 @@ class ElmaxCoordinator(DataUpdateCoordinator[PanelStatus]): def get_actuator_state(self, actuator_id: str) -> Actuator: """Return state of a specific actuator.""" if self._state_by_endpoint is not None: - return self._state_by_endpoint.get(actuator_id) + return self._state_by_endpoint[actuator_id] raise HomeAssistantError("Unknown actuator") def get_zone_state(self, zone_id: str) -> Actuator: """Return state of a specific zone.""" if self._state_by_endpoint is not None: - return self._state_by_endpoint.get(zone_id) + return self._state_by_endpoint[zone_id] raise HomeAssistantError("Unknown zone") + def get_area_state(self, area_id: str) -> Area: + """Return state of a specific area.""" + if self._state_by_endpoint is not None and area_id: + return self._state_by_endpoint[area_id] + raise HomeAssistantError("Unknown area") + @property def http_client(self): """Return the current http client being used by this instance.""" diff --git a/homeassistant/components/elmax/const.py b/homeassistant/components/elmax/const.py index 514412d6897..cd35211e592 100644 --- a/homeassistant/components/elmax/const.py +++ b/homeassistant/components/elmax/const.py @@ -11,7 +11,11 @@ CONF_ELMAX_PANEL_NAME = "panel_name" CONF_CONFIG_ENTRY_ID = "config_entry_id" CONF_ENDPOINT_ID = "endpoint_id" -ELMAX_PLATFORMS = [Platform.SWITCH, Platform.BINARY_SENSOR] +ELMAX_PLATFORMS = [ + Platform.SWITCH, + Platform.BINARY_SENSOR, + Platform.ALARM_CONTROL_PANEL, +] POLLING_SECONDS = 30 DEFAULT_TIMEOUT = 10.0