Followup PR for SIA integration (#51108)

* Updates based on Martin's review

* fix strings and cleaned up constants
This commit is contained in:
Eduard van Valkenburg 2021-05-27 10:55:47 +02:00 committed by GitHub
parent f0952d3ee8
commit cede36d91c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 114 additions and 131 deletions

View file

@ -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))

View file

@ -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

View file

@ -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_{}_{}"

View file

@ -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
]

View file

@ -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"
}

View file

@ -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.",

View file

@ -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,
}

View file

@ -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

View file

@ -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