Skip to content

Commit

Permalink
feat: allow arm/disarm/bypass
Browse files Browse the repository at this point in the history
switch zones to alarm_panels
  • Loading branch information
vermut committed May 8, 2024
1 parent e16c408 commit b375d37
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 93 deletions.
2 changes: 1 addition & 1 deletion custom_components/amc_alarm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 77 additions & 16 deletions custom_components/amc_alarm/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
31 changes: 19 additions & 12 deletions custom_components/amc_alarm/amc_alarm_api/amc_proto.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
51 changes: 36 additions & 15 deletions custom_components/amc_alarm/amc_alarm_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
AmcCommandResponse,
AmcLogin,
AmcCentral,
AmcCentralResponse, CentralDataSections, AmcData, AmcEntry,
AmcCentralResponse,
CentralDataSections,
AmcData,
AmcEntry,
)
from .exceptions import AmcException, ConnectionFailed, AuthenticationFailed

Expand All @@ -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] = {}

Expand Down Expand Up @@ -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

Expand All @@ -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"""
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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",
Expand All @@ -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

Expand Down Expand Up @@ -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
)
47 changes: 0 additions & 47 deletions custom_components/amc_alarm/binary_sensor.py

This file was deleted.

2 changes: 1 addition & 1 deletion custom_components/amc_alarm/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion custom_components/amc_alarm/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions hacs.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "AMC Alarm",
"homeassistant": "2022.5.0",
"render_readme": true,
"zip_release": true,
"filename": "amc_alarm.zip"
Expand Down

0 comments on commit b375d37

Please sign in to comment.