diff --git a/custom_components/amc_alarm/__init__.py b/custom_components/amc_alarm/__init__.py index d4ee2f5..dd3b494 100644 --- a/custom_components/amc_alarm/__init__.py +++ b/custom_components/amc_alarm/__init__.py @@ -39,7 +39,7 @@ async def api_new_data_received_callback(): raise ConfigEntryNotReady("Unable to connect to AMC") from ex async def async_wait_for_states(): - await api.query_states() + await api.command_get_states() for _ in range(30): if api.raw_states(): break diff --git a/custom_components/amc_alarm/alarm_control_panel.py b/custom_components/amc_alarm/alarm_control_panel.py index 97d934b..8b6424c 100644 --- a/custom_components/amc_alarm/alarm_control_panel.py +++ b/custom_components/amc_alarm/alarm_control_panel.py @@ -3,29 +3,34 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, ) +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, + STATE_ALARM_PENDING, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - +from .amc_alarm_api.amc_proto import CentralDataSections from .amc_alarm_api.api import AmcStatesParser from .const import DOMAIN from .entity import AmcBaseEntity async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] states = AmcStatesParser(coordinator.data) - alarms: list[AmcAreaGroup] = [] + alarms: list[AlarmControlPanelEntity] = [] + + def _zone(_central_id, _amc_id): + return lambda raw_state: AmcStatesParser(raw_state).zone(_central_id, _amc_id) def _group(_central_id, _amc_id): return lambda raw_state: AmcStatesParser(raw_state).group(_central_id, _amc_id) @@ -38,31 +43,87 @@ def _area(_central_id, _amc_id): AmcAreaGroup( coordinator=coordinator, amc_entry=x, - attributes_fn=_group(central_id, x.Id) - ) for x in states.groups(central_id).list + attributes_fn=_group(central_id, x.Id), + ) + for x in states.groups(central_id).list ) alarms.extend( AmcAreaGroup( coordinator=coordinator, amc_entry=x, - attributes_fn=_area(central_id, x.Id) - ) for x in states.areas(central_id).list + attributes_fn=_area(central_id, x.Id), + ) + for x in states.areas(central_id).list + ) + alarms.extend( + AmcZone( + coordinator=coordinator, + amc_entry=x, + attributes_fn=_zone(central_id, x.Id), + ) + for x in states.zones(central_id).list ) async_add_entities(alarms, True) +class AmcZone(AmcBaseEntity, AlarmControlPanelEntity): + _attr_code_arm_required = False + _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY + + _amc_group_id = CentralDataSections.ZONES + + async def async_alarm_arm_away(self, code: str | None = None) -> None: + api = self.hass.data[DOMAIN]["__api__"] + await api.command_set_states(self._amc_group_id, self._amc_entry.index, True) + + async def async_alarm_disarm(self, code: str | None = None) -> None: + api = self.hass.data[DOMAIN]["__api__"] + await api.command_set_states(self._amc_group_id, self._amc_entry.index, False) + + @property + def state(self) -> str | None: + if self._amc_entry.states.anomaly: + return STATE_ALARM_TRIGGERED + + match (self._amc_entry.states.bit_armed, self._amc_entry.states.bit_on): + case (1, 1): + return STATE_ALARM_ARMED_AWAY + case (0, 1): + return STATE_ALARM_PENDING + case _: + return STATE_ALARM_DISARMED + + class AmcAreaGroup(AmcBaseEntity, AlarmControlPanelEntity): - _attr_supported_features = ( - # TODO changes AlarmControlPanelEntityFeature.ARM_AWAY - ) + _attr_code_arm_required = False + _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY + _amc_group_id = None + + async def async_alarm_arm_away(self, code: str | None = None) -> None: + api = self.hass.data[DOMAIN]["__api__"] + await api.command_set_states(self._amc_group_id, self._amc_entry.index, True) + + async def async_alarm_disarm(self, code: str | None = None) -> None: + api = self.hass.data[DOMAIN]["__api__"] + await api.command_set_states(self._amc_group_id, self._amc_entry.index, False) @property def state(self) -> str | None: - if self._amc_entry.states.bit_armed: - if self._amc_entry.states.anomaly: + match (self._amc_entry.states.bit_on, self._amc_entry.states.anomaly): + case (1, 1): return STATE_ALARM_TRIGGERED + case (1, 0): + return STATE_ALARM_ARMED_AWAY + case (0, 1): + return STATE_ALARM_PENDING + case (0, 0): + return STATE_ALARM_DISARMED + + +class AmcArea(AmcAreaGroup): + _amc_group_id = CentralDataSections.AREAS - return STATE_ALARM_ARMED_AWAY - return STATE_ALARM_DISARMED +class AmcGroup(AmcAreaGroup): + _amc_group_id = CentralDataSections.GROUPS diff --git a/custom_components/amc_alarm/amc_alarm_api/amc_proto.py b/custom_components/amc_alarm/amc_alarm_api/amc_proto.py index a3b0221..32e940a 100644 --- a/custom_components/amc_alarm/amc_alarm_api/amc_proto.py +++ b/custom_components/amc_alarm/amc_alarm_api/amc_proto.py @@ -1,6 +1,5 @@ -from dataclasses import dataclass from enum import StrEnum -from typing import Union, Optional, List, Dict, Literal, TypeAlias +from typing import Union, Optional, List from pydantic import BaseModel @@ -84,10 +83,18 @@ class AmcLogin(BaseModel): class AmcCommand(BaseModel): command: str - centrals: Optional[List[AmcCentral]] = None data: Optional[AmcLogin] = None token: Optional[str] = None + centrals: Optional[List[AmcCentral]] = None + centralID: Optional[str] = None + centralUsername: Optional[str] = None + centralPassword: Optional[str] = None + + group: Optional[int] = None + index: Optional[int] = None + state: Optional[bool] = None + class AmcCommandResponse(BaseModel): command: str @@ -109,15 +116,15 @@ class CentralDataSections: class SystemStatusDataSections: - GSM_SIGNAL = 0 # _(index=, entity_prefix="GSM Signal") - BATTERY_STATUS = 1 # _(index=, entity_prefix="Battery Status") - POWER = 2 # _(index=, entity_prefix="Power") - PHONE_LINE = 3 # _(index=, entity_prefix="Phone Line") - PANEL_MANIPULATION = 4 # _(index=, entity_prefix="Panel Manipulation") - LINE_MANIPULATION = 5 # _(index=, entity_prefix="Line Manipulation") - PERIPHERALS = 6 # _(index=, entity_prefix="Peripherals") - CONNECTIONS = 7 # _(index=, entity_prefix="Connections") - WIRELESS = 8 # _(index=, entity_prefix="Wireless") + GSM_SIGNAL = 0 # _(index=, entity_prefix="GSM Signal") + BATTERY_STATUS = 1 # _(index=, entity_prefix="Battery Status") + POWER = 2 # _(index=, entity_prefix="Power") + PHONE_LINE = 3 # _(index=, entity_prefix="Phone Line") + PANEL_MANIPULATION = 4 # _(index=, entity_prefix="Panel Manipulation") + LINE_MANIPULATION = 5 # _(index=, entity_prefix="Line Manipulation") + PERIPHERALS = 6 # _(index=, entity_prefix="Peripherals") + CONNECTIONS = 7 # _(index=, entity_prefix="Connections") + WIRELESS = 8 # _(index=, entity_prefix="Wireless") __all__ = [ GSM_SIGNAL, diff --git a/custom_components/amc_alarm/amc_alarm_api/api.py b/custom_components/amc_alarm/amc_alarm_api/api.py index 334fc48..0e5617c 100644 --- a/custom_components/amc_alarm/amc_alarm_api/api.py +++ b/custom_components/amc_alarm/amc_alarm_api/api.py @@ -11,7 +11,10 @@ AmcCommandResponse, AmcLogin, AmcCentral, - AmcCentralResponse, CentralDataSections, AmcData, AmcEntry, + AmcCentralResponse, + CentralDataSections, + AmcData, + AmcEntry, ) from .exceptions import AmcException, ConnectionFailed, AuthenticationFailed @@ -30,13 +33,13 @@ class SimplifiedAmcApi: MAX_FAILED_ATTEMPTS = 60 def __init__( - self, - login_email, - password, - central_id, - central_username, - central_password, - async_state_updated_callback=None, + self, + login_email, + password, + central_id, + central_username, + central_password, + async_state_updated_callback=None, ): self._raw_states: dict[str, AmcCentralResponse] = {} @@ -69,7 +72,7 @@ async def connect(self): continue if self._listen_task.done() and issubclass( - self._listen_task.exception().__class__, AmcException + self._listen_task.exception().__class__, AmcException ): raise self._listen_task.exception() # Something known happened in the listener @@ -79,7 +82,7 @@ async def connect(self): if self._ws_state != ConnectionState.AUTHENTICATED: raise ConnectionFailed() - await self.query_states() + await self.command_get_states() async def _listen(self) -> None: """Listen to messages""" @@ -95,7 +98,7 @@ async def _running(self) -> None: try: _LOGGER.debug("Logging into %s" % self._ws_url) async with session.ws_connect( - self._ws_url, heartbeat=15, autoping=True + self._ws_url, heartbeat=15, autoping=True ) as ws_client: self._ws_state = ConnectionState.CONNECTED self._websocket = ws_client @@ -137,7 +140,8 @@ async def _running(self) -> None: await self._callback() else: _LOGGER.debug( - "Error getting _raw_states: %s" % data.centrals + "Error getting _raw_states: %s" + % data.centrals ) raise AmcException(data.centrals) @@ -188,9 +192,11 @@ async def disconnect(self): async def _send_message(self, msg: AmcCommand): if self._sessionToken: msg.token = self._sessionToken - await self._websocket.send_str(msg.json(exclude_none=True, exclude_unset=True)) + payload = msg.json(exclude_none=True, exclude_unset=True) + _LOGGER.debug("Websocket sending data: %s", payload) + await self._websocket.send_str(payload) - async def query_states(self): + async def command_get_states(self): await self._send_message( AmcCommand( command="getStates", @@ -204,6 +210,19 @@ async def query_states(self): ) ) + async def command_set_states(self, group: int, index: int, state: bool): + await self._send_message( + AmcCommand( + command="setStates", + centralID=self._central_id, + centralUsername=self._central_username, + centralPassword=self._central_password, + group=group, + index=index, + state=state, + ) + ) + def raw_states(self) -> dict[str, AmcCentralResponse]: return self._raw_states @@ -248,4 +267,6 @@ def system_statuses(self, central_id: str) -> AmcData: return self._get_section(central_id, CentralDataSections.SYSTEM_STATUS) def system_status(self, central_id: str, entry_id: int) -> AmcEntry: - return next(x for x in self.system_statuses(central_id).list if x.Id == entry_id) + return next( + x for x in self.system_statuses(central_id).list if x.Id == entry_id + ) diff --git a/custom_components/amc_alarm/binary_sensor.py b/custom_components/amc_alarm/binary_sensor.py deleted file mode 100644 index 12d966c..0000000 --- a/custom_components/amc_alarm/binary_sensor.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import annotations - -from typing import Optional - -from homeassistant.components.binary_sensor import ( - BinarySensorEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .amc_alarm_api.api import AmcStatesParser -from .const import DOMAIN -from .entity import AmcBaseEntity - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_devices: AddEntitiesCallback, -) -> None: - """Set up the binary sensor platform.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - states = AmcStatesParser(coordinator.data) - binary_sensors: list[AmcZone] = [] - - def _zone(_central_id, _amc_id): - return lambda raw_state: AmcStatesParser(raw_state).zone(_central_id, _amc_id) - - for central_id in states.raw_states(): - binary_sensors.extend( - AmcZone( - coordinator=coordinator, - amc_entry=x, - attributes_fn=_zone(central_id, x.Id) - ) for x in states.zones(central_id).list - ) - - async_add_devices(binary_sensors, True) - - -class AmcZone(AmcBaseEntity, BinarySensorEntity): - @property - def is_on(self) -> Optional[bool]: - """Return the state of the sensor.""" - return self._amc_entry.states.bit_opened == 1 diff --git a/custom_components/amc_alarm/const.py b/custom_components/amc_alarm/const.py index 9192b03..5c9e4b4 100644 --- a/custom_components/amc_alarm/const.py +++ b/custom_components/amc_alarm/const.py @@ -5,7 +5,7 @@ NAME = "AMC Alarm" # PLATFORMS SUPPORTED -PLATFORMS = [Platform.BINARY_SENSOR, Platform.ALARM_CONTROL_PANEL] +PLATFORMS = [Platform.ALARM_CONTROL_PANEL] # DATA COORDINATOR ATTRIBUTES LAST_UPDATED = "last_updated" diff --git a/custom_components/amc_alarm/entity.py b/custom_components/amc_alarm/entity.py index 2b315bf..cad61f0 100644 --- a/custom_components/amc_alarm/entity.py +++ b/custom_components/amc_alarm/entity.py @@ -17,7 +17,7 @@ def __init__( self, coordinator: DataUpdateCoordinator, amc_entry: AmcEntry, - attributes_fn: Callable[[dict[str, AmcCentralResponse]], AmcEntry] + attributes_fn: Callable[[dict[str, AmcCentralResponse]], AmcEntry], ) -> None: super().__init__(coordinator) diff --git a/hacs.json b/hacs.json index c6fb193..6bdf3e5 100644 --- a/hacs.json +++ b/hacs.json @@ -1,5 +1,6 @@ { "name": "AMC Alarm", + "homeassistant": "2022.5.0", "render_readme": true, "zip_release": true, "filename": "amc_alarm.zip"