""" Alexa Devices Alarm Control Panel using Guard Mode. SPDX-License-Identifier: Apache-2.0 For more details about this platform, please refer to the documentation at https://community.home-assistant.io/t/echo-devices-alexa-as-media-player-testers-needed/58639 """ from asyncio import sleep import logging from typing import Optional from alexapy import hide_email, hide_serial from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.const import CONF_EMAIL, STATE_UNAVAILABLE from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import CoordinatorEntity from .alexa_entity import parse_guard_state_from_coordinator from .alexa_media import AlexaMedia from .const import ( CONF_EXCLUDE_DEVICES, CONF_INCLUDE_DEVICES, CONF_QUEUE_DELAY, DATA_ALEXAMEDIA, DEFAULT_QUEUE_DELAY, DOMAIN as ALEXA_DOMAIN, ) from .helpers import _catch_login_errors, add_devices, safe_get try: from homeassistant.components.alarm_control_panel import AlarmControlPanelState STATE_ALARM_ARMED_AWAY = AlarmControlPanelState.ARMED_AWAY STATE_ALARM_DISARMED = AlarmControlPanelState.DISARMED except ImportError: from homeassistant.const import STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED _LOGGER = logging.getLogger(__name__) DEPENDENCIES = [ALEXA_DOMAIN] async def async_setup_platform( hass, config, add_devices_callback, discovery_info=None ) -> bool: """Set up the Alexa alarm control panel platform.""" devices: list[AlexaAlarmControlPanel] = [] account = None if config: account = config.get(CONF_EMAIL) if account is None and discovery_info: account = safe_get(discovery_info, ["config", CONF_EMAIL]) if account is None: raise ConfigEntryNotReady include_filter = config.get(CONF_INCLUDE_DEVICES, []) exclude_filter = config.get(CONF_EXCLUDE_DEVICES, []) account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] guard_media_players = {} for key, device in account_dict["devices"]["media_player"].items(): if key not in account_dict["entities"]["media_player"]: _LOGGER.debug( "%s: Media player %s not loaded yet; delaying load", hide_email(account), hide_serial(key), ) raise ConfigEntryNotReady if "GUARD_EARCON" in device["capabilities"]: guard_media_players[key] = account_dict["entities"]["media_player"][key] if "alarm_control_panel" not in (account_dict["entities"]): ( hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"][ "alarm_control_panel" ] ) = {} alexa_client: Optional[AlexaAlarmControlPanel] = None guard_entities = safe_get(account_dict, ["devices", "guard"], []) if guard_entities: alexa_client = AlexaAlarmControlPanel( account_dict["login_obj"], account_dict["coordinator"], guard_entities[0], guard_media_players, ) else: _LOGGER.debug("%s: No Alexa Guard entity found", hide_email(account)) if not (alexa_client and alexa_client.unique_id): _LOGGER.debug( "%s: Skipping creation of uninitialized device: %s", hide_email(account), alexa_client, ) elif alexa_client.unique_id not in ( account_dict["entities"]["alarm_control_panel"] ): devices.append(alexa_client) ( hass.data[DATA_ALEXAMEDIA]["accounts"][account]["entities"][ "alarm_control_panel" ][alexa_client.unique_id] ) = alexa_client else: _LOGGER.debug( "%s: Skipping already added device: %s", hide_email(account), alexa_client ) return await add_devices( hide_email(account), devices, add_devices_callback, include_filter, exclude_filter, ) async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the Alexa alarm control panel platform by config_entry.""" return await async_setup_platform( hass, config_entry.data, async_add_devices, discovery_info=None ) async def async_unload_entry(hass, entry) -> bool: """Unload a config entry.""" account = entry.data[CONF_EMAIL] _LOGGER.debug("Attempting to unload alarm control panel") account_dict = hass.data[DATA_ALEXAMEDIA]["accounts"][account] for device in account_dict["entities"]["alarm_control_panel"].values(): _LOGGER.debug("Removing %s", device) await device.async_remove() return True class AlexaAlarmControlPanel(AlarmControlPanelEntity, AlexaMedia, CoordinatorEntity): """Implementation of Alexa Media Player alarm control panel.""" def __init__(self, login, coordinator, guard_entity, media_players=None) -> None: """Initialize the Alexa device.""" AlexaMedia.__init__(self, None, login) CoordinatorEntity.__init__(self, coordinator) _LOGGER.debug("%s: Initiating alarm control panel", hide_email(login.email)) # AlexaAPI requires a AlexaClient object, need to clean this up # Guard info self._appliance_id = guard_entity["appliance_id"] self._guard_entity_id = guard_entity["id"] self._friendly_name = "Alexa Guard " + self._appliance_id[-5:] self._media_players = {} or media_players self._attrs: dict[str, str] = {} _LOGGER.debug( "%s: Guard Discovered %s: %s %s", self.account, self._friendly_name, hide_serial(self._appliance_id), hide_serial(self._guard_entity_id), ) @_catch_login_errors async def _async_alarm_set( self, command: str = "", code=None, # pylint: disable=unused-argument ) -> None: """Send command.""" try: if not self.enabled: return except AttributeError: pass if command not in (STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED): _LOGGER.error("Invalid command: %s", command) return command_map = {STATE_ALARM_ARMED_AWAY: "AWAY", STATE_ALARM_DISARMED: "HOME"} available_media_players = list( filter(lambda x: x.state != STATE_UNAVAILABLE, self._media_players.values()) ) if available_media_players: _LOGGER.debug("Sending guard command to: %s", available_media_players[0]) available_media_players[0].check_login_changes() # Extract appliance ID safely to prevent IndexError if format is unexpected appliance_parts = self._appliance_id.split("_") appliance_id = ( appliance_parts[2] if len(appliance_parts) > 2 else self._appliance_id ) await available_media_players[0].alexa_api.set_guard_state( appliance_id, command_map[command], queue_delay=self.hass.data[DATA_ALEXAMEDIA]["accounts"][self.email][ "options" ].get(CONF_QUEUE_DELAY, DEFAULT_QUEUE_DELAY), ) await sleep(2) # delay else: _LOGGER.debug("Performing static guard command") await self.alexa_api.static_set_guard_state( self._login, self._guard_entity_id, command ) await self.coordinator.async_request_refresh() async def async_alarm_disarm( self, code=None, # pylint:disable=unused-argument ) -> None: """Send disarm command.""" await self._async_alarm_set(STATE_ALARM_DISARMED) async def async_alarm_arm_away( self, code=None, # pylint:disable=unused-argument ) -> None: """Send arm away command.""" await self._async_alarm_set(STATE_ALARM_ARMED_AWAY) @property def unique_id(self): """Return the unique ID.""" return self._guard_entity_id @property def name(self): """Return the name of the device.""" return self._friendly_name @property def state(self): """Return the state of the device.""" _state = parse_guard_state_from_coordinator( self.coordinator, self._guard_entity_id ) if _state == "ARMED_AWAY": return STATE_ALARM_ARMED_AWAY return STATE_ALARM_DISARMED @property def supported_features(self) -> int: """Return the list of supported features.""" # pylint: disable=import-outside-toplevel try: from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, ) except ImportError: return 0 return AlarmControlPanelEntityFeature.ARM_AWAY @property def assumed_state(self) -> bool: """Return assumed state. Returns bool: Whether the state is assumed """ last_refresh_success = ( self.coordinator.data and self._guard_entity_id in self.coordinator.data ) return not last_refresh_success @property def extra_state_attributes(self): """Return the state attributes.""" return self._attrs