From cede36d91c6dce4109791cce2de30a5f45830f08 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 27 May 2021 10:55:47 +0200 Subject: [PATCH] Followup PR for SIA integration (#51108) * Updates based on Martin's review * fix strings and cleaned up constants --- .../components/sia/alarm_control_panel.py | 62 +++++-------- homeassistant/components/sia/config_flow.py | 30 +++---- homeassistant/components/sia/const.py | 34 +++----- homeassistant/components/sia/hub.py | 24 +++-- homeassistant/components/sia/manifest.json | 2 +- homeassistant/components/sia/strings.json | 2 +- homeassistant/components/sia/utils.py | 87 +++++++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 114 insertions(+), 131 deletions(-) diff --git a/homeassistant/components/sia/alarm_control_panel.py b/homeassistant/components/sia/alarm_control_panel.py index 9d5f62b02de..fe5b95b639e 100644 --- a/homeassistant/components/sia/alarm_control_panel.py +++ b/homeassistant/components/sia/alarm_control_panel.py @@ -2,18 +2,14 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any from pysiaalarm import SIAEvent -from homeassistant.components.alarm_control_panel import ( - ENTITY_ID_FORMAT as ALARM_ENTITY_ID_FORMAT, - AlarmControlPanelEntity, -) +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_PORT, - CONF_ZONE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_NIGHT, @@ -21,8 +17,10 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType @@ -33,7 +31,6 @@ from .const import ( CONF_PING_INTERVAL, CONF_ZONES, DOMAIN, - SIA_ENTITY_ID_FORMAT, SIA_EVENT, SIA_NAME_FORMAT, SIA_UNIQUE_ID_FORMAT_ALARM, @@ -76,21 +73,17 @@ CODE_CONSEQUENCES: dict[str, StateType] = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[..., None], -) -> bool: + async_add_entities: AddEntitiesCallback, +) -> None: """Set up SIA alarm_control_panel(s) from a config entry.""" async_add_entities( - [ - SIAAlarmControlPanel(entry, account_data, zone) - for account_data in entry.data[CONF_ACCOUNTS] - for zone in range( - 1, - entry.options[CONF_ACCOUNTS][account_data[CONF_ACCOUNT]][CONF_ZONES] - + 1, - ) - ] + SIAAlarmControlPanel(entry, account_data, zone) + for account_data in entry.data[CONF_ACCOUNTS] + for zone in range( + 1, + entry.options[CONF_ACCOUNTS][account_data[CONF_ACCOUNT]][CONF_ZONES] + 1, + ) ) - return True class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): @@ -111,18 +104,7 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): self._account: str = self._account_data[CONF_ACCOUNT] self._ping_interval: int = self._account_data[CONF_PING_INTERVAL] - self.entity_id: str = ALARM_ENTITY_ID_FORMAT.format( - SIA_ENTITY_ID_FORMAT.format( - self._port, self._account, self._zone, DEVICE_CLASS_ALARM - ) - ) - - self._attr: dict[str, Any] = { - CONF_PORT: self._port, - CONF_ACCOUNT: self._account, - CONF_ZONE: self._zone, - CONF_PING_INTERVAL: f"{self._ping_interval} minute(s)", - } + self._attr: dict[str, Any] = {} self._available: bool = True self._state: StateType = None @@ -134,16 +116,17 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): Overridden from Entity. - 1. start the event listener and add the callback to on_remove + 1. register the dispatcher and add the callback to on_remove 2. get previous state from storage 3. if previous state: restore 4. if previous state is unavailable: set _available to False and return 5. if available: create availability cb """ self.async_on_remove( - self.hass.bus.async_listen( - event_type=SIA_EVENT.format(self._port, self._account), - listener=self.async_handle_event, + async_dispatcher_connect( + self.hass, + SIA_EVENT.format(self._port, self._account), + self.async_handle_event, ) ) last_state = await self.async_get_last_state() @@ -162,14 +145,11 @@ class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): if self._cancel_availability_cb: self._cancel_availability_cb() - async def async_handle_event(self, event: Event) -> None: - """Listen to events for this port and account and update state and attributes. + async def async_handle_event(self, sia_event: SIAEvent) -> None: + """Listen to dispatcher events for this port and account and update state and attributes. If the port and account combo receives any message it means it is online and can therefore be set to available. """ - sia_event: SIAEvent = SIAEvent.from_dict( # pylint: disable=no-member - event.data - ) _LOGGER.debug("Received event: %s", sia_event) if int(sia_event.ri) == self._zone: self._attr.update(get_attr_from_sia_event(sia_event)) diff --git a/homeassistant/components/sia/config_flow.py b/homeassistant/components/sia/config_flow.py index fe49ec65777..a9b49765c19 100644 --- a/homeassistant/components/sia/config_flow.py +++ b/homeassistant/components/sia/config_flow.py @@ -18,6 +18,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PORT, CONF_PROTOCOL from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.typing import ConfigType from .const import ( @@ -104,7 +105,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data: ConfigType = {} self._options: Mapping[str, Any] = {CONF_ACCOUNTS: {}} - async def async_step_user(self, user_input: ConfigType = None): + async def async_step_user(self, user_input: ConfigType = None) -> FlowResult: """Handle the initial user step.""" errors: dict[str, str] | None = None if user_input is not None: @@ -115,7 +116,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_handle_data_and_route(user_input) - async def async_step_add_account(self, user_input: ConfigType = None): + async def async_step_add_account(self, user_input: ConfigType = None) -> FlowResult: """Handle the additional accounts steps.""" errors: dict[str, str] | None = None if user_input is not None: @@ -126,11 +127,11 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_handle_data_and_route(user_input) - async def async_handle_data_and_route(self, user_input: ConfigType): + async def async_handle_data_and_route(self, user_input: ConfigType) -> FlowResult: """Handle the user_input, check if configured and route to the right next step or create entry.""" self._update_data(user_input) - if self._data and self._port_already_configured(): - return self.async_abort(reason="already_configured") + + self._async_abort_entries_match({CONF_PORT: self._data[CONF_PORT]}) if user_input[CONF_ADDITIONAL_ACCOUNTS]: return await self.async_step_add_account() @@ -163,13 +164,6 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._options[CONF_ACCOUNTS].setdefault(account, deepcopy(DEFAULT_OPTIONS)) self._options[CONF_ACCOUNTS][account][CONF_ZONES] = user_input[CONF_ZONES] - def _port_already_configured(self): - """See if we already have a SIA entry matching the port.""" - for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_PORT] == self._data[CONF_PORT]: - return True - return False - class SIAOptionsFlowHandler(config_entries.OptionsFlow): """Handle SIA options.""" @@ -181,14 +175,15 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.hub: SIAHub | None = None self.accounts_todo: list = [] - async def async_step_init(self, user_input: ConfigType = None): + async def async_step_init(self, user_input: ConfigType = None) -> FlowResult: """Manage the SIA options.""" self.hub = self.hass.data[DOMAIN][self.config_entry.entry_id] - if self.hub is not None and self.hub.sia_accounts is not None: - self.accounts_todo = [a.account_id for a in self.hub.sia_accounts] - return await self.async_step_options() + assert self.hub is not None + assert self.hub.sia_accounts is not None + self.accounts_todo = [a.account_id for a in self.hub.sia_accounts] + return await self.async_step_options() - async def async_step_options(self, user_input: ConfigType = None): + async def async_step_options(self, user_input: ConfigType = None) -> FlowResult: """Create the options step for a account.""" errors: dict[str, str] | None = None if user_input is not None: @@ -223,7 +218,6 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.options[CONF_ACCOUNTS][account][CONF_ZONES] = user_input[CONF_ZONES] if self.accounts_todo: return await self.async_step_options() - _LOGGER.warning("Updating SIA Options with %s", self.options) return self.async_create_entry(title="", data=self.options) @property diff --git a/homeassistant/components/sia/const.py b/homeassistant/components/sia/const.py index ceeaac75923..916cdb9621c 100644 --- a/homeassistant/components/sia/const.py +++ b/homeassistant/components/sia/const.py @@ -5,34 +5,24 @@ from homeassistant.components.alarm_control_panel import ( PLATFORMS = [ALARM_CONTROL_PANEL_DOMAIN] +DOMAIN = "sia" + +ATTR_CODE = "last_code" +ATTR_ZONE = "zone" +ATTR_MESSAGE = "last_message" +ATTR_ID = "last_id" +ATTR_TIMESTAMP = "last_timestamp" + +TITLE = "SIA Alarm on port {}" CONF_ACCOUNT = "account" CONF_ACCOUNTS = "accounts" CONF_ADDITIONAL_ACCOUNTS = "additional_account" -CONF_PING_INTERVAL = "ping_interval" CONF_ENCRYPTION_KEY = "encryption_key" -CONF_ZONES = "zones" CONF_IGNORE_TIMESTAMPS = "ignore_timestamps" +CONF_PING_INTERVAL = "ping_interval" +CONF_ZONES = "zones" -DOMAIN = "sia" -TITLE = "SIA Alarm on port {}" -SIA_EVENT = "sia_event_{}_{}" SIA_NAME_FORMAT = "{} - {} - zone {} - {}" -SIA_NAME_FORMAT_HUB = "{} - {} - {}" -SIA_ENTITY_ID_FORMAT = "{}_{}_{}_{}" -SIA_ENTITY_ID_FORMAT_HUB = "{}_{}_{}" SIA_UNIQUE_ID_FORMAT_ALARM = "{}_{}_{}" -SIA_UNIQUE_ID_FORMAT = "{}_{}_{}_{}" -HUB_SENSOR_NAME = "last_heartbeat" -HUB_ZONE = 0 -PING_INTERVAL_MARGIN = 30 -DEFAULT_TIMEBAND = (80, 40) -IGNORED_TIMEBAND = (3600, 1800) - -EVENT_CODE = "last_code" -EVENT_ACCOUNT = "account" -EVENT_ZONE = "zone" -EVENT_PORT = "port" -EVENT_MESSAGE = "last_message" -EVENT_ID = "last_id" -EVENT_TIMESTAMP = "last_timestamp" +SIA_EVENT = "sia_event_{}_{}" diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index e5dc7b85ed8..387c2273606 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -9,8 +9,9 @@ from pysiaalarm.aio import CommunicationsProtocol, SIAAccount, SIAClient, SIAEve from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PORT, CONF_PROTOCOL, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, EventOrigin, HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( CONF_ACCOUNT, @@ -18,16 +19,19 @@ from .const import ( CONF_ENCRYPTION_KEY, CONF_IGNORE_TIMESTAMPS, CONF_ZONES, - DEFAULT_TIMEBAND, DOMAIN, - IGNORED_TIMEBAND, PLATFORMS, SIA_EVENT, ) +from .utils import get_event_data_from_sia_event _LOGGER = logging.getLogger(__name__) +DEFAULT_TIMEBAND = (80, 40) +IGNORED_TIMEBAND = (3600, 1800) + + class SIAHub: """Class for SIA Hubs.""" @@ -39,7 +43,7 @@ class SIAHub: """Create the SIAHub.""" self._hass: HomeAssistant = hass self._entry: ConfigEntry = entry - self._port: int = int(entry.data[CONF_PORT]) + self._port: int = entry.data[CONF_PORT] self._title: str = entry.title self._accounts: list[dict[str, Any]] = deepcopy(entry.data[CONF_ACCOUNTS]) self._protocol: str = entry.data[CONF_PROTOCOL] @@ -69,21 +73,23 @@ class SIAHub: await self.sia_client.stop() async def async_create_and_fire_event(self, event: SIAEvent) -> None: - """Create a event on HA's bus, with the data from the SIAEvent. + """Create a event on HA dispatcher and then on HA's bus, with the data from the SIAEvent. The created event is handled by default for only a small subset for each platform (there are about 320 SIA Codes defined, only 22 of those are used in the alarm_control_panel), a user can choose to build other automation or even entities on the same event for SIA codes not handled by the built-in platforms. """ _LOGGER.debug( - "Adding event to bus for code %s for port %s and account %s", + "Adding event to dispatch and bus for code %s for port %s and account %s", event.code, self._port, event.account, ) + async_dispatcher_send( + self._hass, SIA_EVENT.format(self._port, event.account), event + ) self._hass.bus.async_fire( event_type=SIA_EVENT.format(self._port, event.account), - event_data=event.to_dict(encode_json=True), - origin=EventOrigin.remote, + event_data=get_event_data_from_sia_event(event), ) def update_accounts(self): @@ -115,7 +121,7 @@ class SIAHub: options = dict(self._entry.options) for acc in self._accounts: acc_id = acc[CONF_ACCOUNT] - if acc_id in options[CONF_ACCOUNTS].keys(): + if acc_id in options[CONF_ACCOUNTS]: acc[CONF_IGNORE_TIMESTAMPS] = options[CONF_ACCOUNTS][acc_id][ CONF_IGNORE_TIMESTAMPS ] diff --git a/homeassistant/components/sia/manifest.json b/homeassistant/components/sia/manifest.json index 67c2a0e91a1..eaeb4547167 100644 --- a/homeassistant/components/sia/manifest.json +++ b/homeassistant/components/sia/manifest.json @@ -3,7 +3,7 @@ "name": "SIA Alarm Systems", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sia", - "requirements": ["pysiaalarm==3.0.0b12"], + "requirements": ["pysiaalarm==3.0.0"], "codeowners": ["@eavanvalkenburg"], "iot_class": "local_push" } diff --git a/homeassistant/components/sia/strings.json b/homeassistant/components/sia/strings.json index b091fdd341d..f837d41056a 100644 --- a/homeassistant/components/sia/strings.json +++ b/homeassistant/components/sia/strings.json @@ -27,7 +27,7 @@ }, "error": { "invalid_key_format": "The key is not a hex value, please use only 0-9 and A-F.", - "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 characters hex characters.", + "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 hex characters.", "invalid_account_format": "The account is not a hex value, please use only 0-9 and A-F.", "invalid_account_length": "The account is not the right length, it has to be between 3 and 16 characters.", "invalid_ping": "The ping interval needs to be between 1 and 1440 minutes.", diff --git a/homeassistant/components/sia/utils.py b/homeassistant/components/sia/utils.py index 9b02025aa8d..08e0fce8ab2 100644 --- a/homeassistant/components/sia/utils.py +++ b/homeassistant/components/sia/utils.py @@ -6,19 +6,9 @@ from typing import Any from pysiaalarm import SIAEvent -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from .const import ATTR_CODE, ATTR_ID, ATTR_MESSAGE, ATTR_TIMESTAMP, ATTR_ZONE -from .const import ( - EVENT_ACCOUNT, - EVENT_CODE, - EVENT_ID, - EVENT_MESSAGE, - EVENT_TIMESTAMP, - EVENT_ZONE, - HUB_SENSOR_NAME, - HUB_ZONE, - PING_INTERVAL_MARGIN, -) +PING_INTERVAL_MARGIN = 30 def get_unavailability_interval(ping: int) -> float: @@ -26,32 +16,55 @@ def get_unavailability_interval(ping: int) -> float: return timedelta(minutes=ping, seconds=PING_INTERVAL_MARGIN).total_seconds() -def get_name(port: int, account: str, zone: int, entity_type: str) -> str: - """Give back a entity_id and name according to the variables.""" - if zone == HUB_ZONE: - return f"{port} - {account} - {'Last Heartbeat' if entity_type == DEVICE_CLASS_TIMESTAMP else 'Power'}" - return f"{port} - {account} - zone {zone} - {entity_type}" - - -def get_entity_id(port: int, account: str, zone: int, entity_type: str) -> str: - """Give back a entity_id according to the variables.""" - if zone == HUB_ZONE: - return f"{port}_{account}_{HUB_SENSOR_NAME if entity_type == DEVICE_CLASS_TIMESTAMP else entity_type}" - return f"{port}_{account}_{zone}_{entity_type}" - - -def get_unique_id(entry_id: str, account: str, zone: int, domain: str) -> str: - """Return the unique id.""" - return f"{entry_id}_{account}_{zone}_{domain}" - - def get_attr_from_sia_event(event: SIAEvent) -> dict[str, Any]: """Create the attributes dict from a SIAEvent.""" return { - EVENT_ACCOUNT: event.account, - EVENT_ZONE: event.ri, - EVENT_CODE: event.code, - EVENT_MESSAGE: event.message, - EVENT_ID: event.id, - EVENT_TIMESTAMP: event.timestamp, + ATTR_ZONE: event.ri, + ATTR_CODE: event.code, + ATTR_MESSAGE: event.message, + ATTR_ID: event.id, + ATTR_TIMESTAMP: event.timestamp.isoformat(), + } + + +def get_event_data_from_sia_event(event: SIAEvent) -> dict[str, Any]: + """Create a dict from the SIA Event for the HA Event.""" + return { + "message_type": event.message_type, + "receiver": event.receiver, + "line": event.line, + "account": event.account, + "sequence": event.sequence, + "content": event.content, + "ti": event.ti, + "id": event.id, + "ri": event.ri, + "code": event.code, + "message": event.message, + "x_data": event.x_data, + "timestamp": event.timestamp.isoformat(), + "event_qualifier": event.qualifier, + "event_type": event.event_type, + "partition": event.partition, + "extended_data": [ + { + "identifier": xd.identifier, + "name": xd.name, + "description": xd.description, + "length": xd.length, + "characters": xd.characters, + "value": xd.value, + } + for xd in event.extended_data + ] + if event.extended_data is not None + else None, + "sia_code": { + "code": event.sia_code.code, + "type": event.sia_code.type, + "description": event.sia_code.description, + "concerns": event.sia_code.concerns, + } + if event.sia_code is not None + else None, } diff --git a/requirements_all.txt b/requirements_all.txt index 2bd8f06a618..29a490cfc43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1723,7 +1723,7 @@ pysesame2==1.0.1 pysher==1.0.1 # homeassistant.components.sia -pysiaalarm==3.0.0b12 +pysiaalarm==3.0.0 # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c5b3a826ffc..1caa6ba7b8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -962,7 +962,7 @@ pyserial-asyncio==0.5 pyserial==3.5 # homeassistant.components.sia -pysiaalarm==3.0.0b12 +pysiaalarm==3.0.0 # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4