Followup PR for SIA integration (#51108)
* Updates based on Martin's review * fix strings and cleaned up constants
This commit is contained in:
parent
f0952d3ee8
commit
cede36d91c
9 changed files with 114 additions and 131 deletions
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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_{}_{}"
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue