diff --git a/README.md b/README.md index fb1e689..2aeca33 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ _Component to integrate with [SIA][sia], based on [CheaterDev's version][ch_sia]._ +_Latest beta will be suggested for inclusion as a official integration._ + **This component will set up the following platforms.** ## WARNING @@ -10,22 +12,22 @@ This integration was tested with Ajax Systems security hub only. Other SIA hubs Platform | Description -- | -- -`binary_sensor` | A smoke or moisture sensor. -`alarm_control_panel` | Alarm panel with the state of the alarm. -`sensor` | Sensor with the last heartbeat message from your system. +`binary_sensor` | A smoke and moisture sensor, one of each per account and zone. +`alarm_control_panel` | Alarm panel with the state of the alarm, one per account and zone. +`sensor` | Sensor with the last heartbeat message from your system, one per account. ## Features - Alarm tracking with a alarm_control_panel component -- Optional Fire/gas tracker -- Optional Water leak tracker +- Fire/gas tracker +- Water leak tracker - AES-128 CBC encryption support -## Hub Setup(Ajax Systems Hub example) +## Hub Setup (Ajax Systems Hub example) 1. Select "SIA Protocol". 2. Enable "Connect on demand". 3. Place Account Id - 3-16 ASCII hex characters. For example AAA. -4. Insert Home Assistant IP adress. It must be visible to hub. There is no cloud connection to it. +4. Insert Home Assistant IP address. It must be a visible to hub. There is no cloud connection to it. 5. Insert Home Assistant listening port. This port must not be used with anything else. 6. Select Preferred Network. Ethernet is preferred if hub and HA in same network. Multiple networks are not tested. 7. Enable Periodic Reports. The interval with which the alarm systems reports to the monitoring station, default is 1 minute. This component adds 30 seconds before setting the alarm unavailable to deal with slights latencies between ajax and HA and the async nature of HA. @@ -34,52 +36,23 @@ Platform | Description ## Installation 1. Click install. -1. Add at least the minimum configuration to your HA configuration, see below. - -### Minimum config -This is the least amount of information that needs to be in your config. This will result in a `sensor.hubname_last_heartbeat` being added after reboot. Dynamically any other sensors are added. - -```yaml -sia: - port: port - hubs: - - name: hubname - account: account -``` - -## Full configuration - -```yaml -sia: - port: port - hubs: - - name: hubname - account: account - encryption_key: password - ping_interval: pinginterval - zones: - - zone: 1 - name: zonename - sensors: - - alarm - - moisture - - smoke -``` +1. The latest version is only available through a config flow. +1. After clicking the add button in the Integration pane, you full in the below fields. + +If you have multiple accounts that you want to monitor you can choose to have both communicating with the same port, in that case, use the additional accounts checkbox in the config so setup the second (and more) accounts. You can also choose to have both running on a different port, in that case setup the component twice. + +After setup you will see one entity per account for the heartbeat, and 3 entities for each zone per account, alarm, smoke sensor and moisture sensor. This means at least four entities are added, each will also have a device associated with it, so allow you to use the area feature. Unwanted sensors should be hidden in the interface. ## Configuration options Key | Type | Required | Description -- | -- | -- | -- `port` | `int` | `True` | Port that SIA will listen on. -`hubs` | `list` | `True` | List of all hubs to connect to. -`name` | `string` | `True` | Used to generate sensor ids. `account` | `string` | `True` | Hub account to track. 3-16 ASCII hex characters. Must be same, as in hub properties. `encryption_key` | `string` | `False` | Encoding key. 16 ASCII characters. Must be same, as in hub properties. -`ping_interval` | `int` | `False` | Ping interval in minutes that the alarm system uses to send "Automatic communication test report" messages, the HA component adds 30 seconds before marking a device unavailable. Must be between 1 and 1440 minutes. -`zones` | `list` | `False` | Manual definition of all zones present, if unspecified, only the hub sensor is added, and new sensors are added based on messages coming in. -`zone` | `int` | `False` | ZoneID, must match the zone that the system sends, can be found in the log but also "discovered" -`name` | `string` | `False` | Zone name, is used for the friendly name of your sensors, when you have the same sensortypes in multiple zones and this is not set, a `_1, _2, etc` is added by HA automatically. -`sensors` | `list` | `False` | a list of sensors, must be of type: `alarm`, `moisture` (HA standard name for a leak sensor) or `smoke` +`ping_interval` | `int` | `True` | Ping interval in minutes that the alarm system uses to send "Automatic communication test report" messages, the HA component adds 30 seconds before marking a device unavailable. Must be between 1 and 1440 minutes, default is 1. +`zones` | `int` | `True` | The number of zones present for the account, default is 1. +`additional_account` | `bool` | `True` | Used to ask for additional accounts in multiple steps during setup, default is False. ASCII characters are 0-9 and ABCDEF, so a account is something like `346EB` and the encryption key is the same but 16 characters. *** diff --git a/custom_components/sia/__init__.py b/custom_components/sia/__init__.py index c2348c5..bd98da8 100644 --- a/custom_components/sia/__init__.py +++ b/custom_components/sia/__init__.py @@ -1,457 +1,269 @@ -"""Module for SIA Hub.""" - +"""The sia integration.""" import asyncio -import base64 -from binascii import hexlify, unhexlify -from collections import defaultdict -from datetime import datetime, timedelta -import json +from datetime import timedelta import logging -import random -import re -import socketserver -import string -import sys -import threading -from threading import Thread -import time -from Crypto import Random -from Crypto.Cipher import AES -import requests -from requests_toolbelt.utils import dump -import sseclient -import voluptuous as vol +from pysiaalarm.aio import SIAAccount, SIAClient, SIAEvent -from homeassistant.components.alarm_control_panel import ( - ENTITY_ID_FORMAT as ALARM_FORMAT, - AlarmControlPanel, -) from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOISTURE, DEVICE_CLASS_SMOKE, - ENTITY_ID_FORMAT as BINARY_SENSOR_FORMAT, - BinarySensorDevice, ) -from homeassistant.components.sensor import ENTITY_ID_FORMAT as SENSOR_FORMAT +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_FRIENDLY_NAME, - CONF_NAME, CONF_PORT, CONF_SENSORS, CONF_ZONE, DEVICE_CLASS_TIMESTAMP, - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_CUSTOM_BYPASS, - STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, - STATE_OFF, - STATE_ON, -) -from homeassistant.core import callback -from homeassistant.exceptions import TemplateError -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity, generate_entity_id -from homeassistant.helpers.event import ( - async_track_point_in_utc_time, - async_track_state_change, + EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers import device_registry as dr from homeassistant.util.dt import utcnow +from homeassistant.util.json import load_json -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "sia" -DATA_UPDATED = f"{DOMAIN}_data_updated" -CONF_ACCOUNT = "account" -CONF_ENCRYPTION_KEY = "encryption_key" -CONF_HUBS = "hubs" -CONF_PING_INTERVAL = "ping_interval" -CONF_ZONES = "zones" - -DEVICE_CLASS_ALARM = "alarm" -HUB_SENSOR_NAME = "_last_heartbeat" -HUB_ZONE = 0 - -TYPES = [DEVICE_CLASS_ALARM, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_SMOKE] - -ZONE_CONFIG = vol.Schema( - { - vol.Optional(CONF_ZONE, default=1): cv.positive_int, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_SENSORS, default=[DEVICE_CLASS_ALARM]): vol.All( - cv.ensure_list, [vol.In(TYPES)] - ), - } -) - -HUB_CONFIG = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ACCOUNT): cv.string, - vol.Optional(CONF_ENCRYPTION_KEY): cv.string, - vol.Optional(CONF_PING_INTERVAL, default=1): vol.All( - vol.Coerce(int), vol.Range(min=1, max=1440) - ), - vol.Optional(CONF_ZONES, default=[]): vol.All(cv.ensure_list, [ZONE_CONFIG]), - } -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_PORT): cv.string, - vol.Required(CONF_HUBS, default={}): vol.All( - cv.ensure_list, [HUB_CONFIG] - ), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - -ID_R = "\r".encode() - -PING_INTERVAL_MARGIN = timedelta(seconds=30) - -HASS_PLATFORM = None - -# final import here, because they rely on variables above -from .sia_event import SIAEvent from .alarm_control_panel import SIAAlarmControlPanel from .binary_sensor import SIABinarySensor +from .const import ( + CONF_ACCOUNT, + CONF_ACCOUNTS, + CONF_ENCRYPTION_KEY, + CONF_PING_INTERVAL, + CONF_ZONES, + DEVICE_CLASS_ALARM, + DOMAIN, + HUB_SENSOR_NAME, + HUB_ZONE, + LAST_MESSAGE, + PLATFORMS, + REACTIONS, + UTCNOW, +) from .sensor import SIASensor +_LOGGER = logging.getLogger(__name__) -def setup(hass, config): - """Implementation of setup from HA.""" - global HASS_PLATFORM - socketserver.TCPServer.allow_reuse_address = True - HASS_PLATFORM = hass - HASS_PLATFORM.data[DOMAIN] = {} +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the sia component.""" + hass.data[DOMAIN] = {} + return True - port = int(config[DOMAIN][CONF_PORT]) - for hub_config in config[DOMAIN][CONF_HUBS]: - hass.data[DOMAIN][hub_config[CONF_ACCOUNT]] = Hub(hass, hub_config) +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up sia from a config entry.""" + hass.data[DOMAIN][entry.entry_id] = SIAHub( + hass, entry.data, entry.entry_id, entry.title + ) + await hass.data[DOMAIN][entry.entry_id].async_setup_hub() + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + hass.data[DOMAIN][entry.entry_id].sia_client.start(reuse_port=True) + return True - for component in ["binary_sensor", "alarm_control_panel", "sensor"]: - discovery.load_platform(hass, component, DOMAIN, {}, config) - for hub in HASS_PLATFORM.data[DOMAIN].values(): - for sensor in hub._states.values(): - sensor.async_schedule_update_ha_state() +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" - server = socketserver.TCPServer(("", port), AlarmTCPHandler) + await hass.data[DOMAIN][entry.entry_id].sia_client.stop() + hass.data[DOMAIN][entry.entry_id].shutdown_remove_listener() + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) - server_thread = threading.Thread(target=server.serve_forever) - server_thread.start() + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) - return True + return unload_ok -class Hub: +class SIAHub: """Class for SIA Hubs.""" - sensor_types_classes = { - DEVICE_CLASS_ALARM: "SIAAlarmControlPanel", - DEVICE_CLASS_MOISTURE: "SIABinarySensor", - DEVICE_CLASS_SMOKE: "SIABinarySensor", - DEVICE_CLASS_TIMESTAMP: "SIASensor", - } - - # main set of responses to certain codes from SIA (see sia_codes for all of them) - reactions = { - "BA": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_TRIGGERED}, - "BR": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_DISARMED}, - "CA": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_ARMED_AWAY}, - "CF": { - "type": DEVICE_CLASS_ALARM, - "new_state": STATE_ALARM_ARMED_CUSTOM_BYPASS, - }, - "CG": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_ARMED_AWAY}, - "CL": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_ARMED_AWAY}, - "CP": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_ARMED_AWAY}, - "CQ": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_ARMED_AWAY}, - "GA": {"type": DEVICE_CLASS_SMOKE, "new_state": True}, - "GH": {"type": DEVICE_CLASS_SMOKE, "new_state": False}, - "NL": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_ARMED_NIGHT}, - "OA": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_DISARMED}, - "OG": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_DISARMED}, - "OP": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_DISARMED}, - "OQ": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_DISARMED}, - "OR": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_DISARMED}, - "RP": {"type": DEVICE_CLASS_TIMESTAMP, "new_state_eval": "utcnow()"}, - "TA": {"type": DEVICE_CLASS_ALARM, "new_state": STATE_ALARM_TRIGGERED}, - "WA": {"type": DEVICE_CLASS_MOISTURE, "new_state": True}, - "WH": {"type": DEVICE_CLASS_MOISTURE, "new_state": False}, - "YG": {"type": DEVICE_CLASS_TIMESTAMP, "attr": True}, - } - - def __init__(self, hass, hub_config): - self._name = hub_config[CONF_NAME] - self._account_id = hub_config[CONF_ACCOUNT] + def __init__( + self, hass: HomeAssistant, hub_config: dict, entry_id: str, title: str + ): + """Create the SIAHub.""" self._hass = hass - self._states = {} - self._zones = [dict(z) for z in hub_config.get(CONF_ZONES)] - self._ping_interval = timedelta(minutes=hub_config.get(CONF_PING_INTERVAL)) - self._encrypted = False - self._ending = "]" - self._key = hub_config.get(CONF_ENCRYPTION_KEY) - if self._key: - _LOGGER.debug("Hub: init: encryption is enabled.") - self._encrypted = True - self._key = self._key.encode("utf8") - # IV standards from https://manualzz.com/doc/11555754/sia-digital-communication-standard-%E2%80%93-internet-protocol-ev... - # page 12 specifies the decrytion IV to all zeros. - self._decrypter = AES.new( - self._key, AES.MODE_CBC, unhexlify("00000000000000000000000000000000") - ) - _encrypter = AES.new( - self._key, AES.MODE_CBC, Random.new().read(AES.block_size) - ) - self._ending = ( - hexlify(_encrypter.encrypt("00000000000000|]".encode("utf8"))) - .decode(encoding="UTF-8") - .upper() - ) - # add sensors for each zone as specified in the config. - for zone in self._zones: - for sensor in zone.get(CONF_SENSORS): - self._upsert_sensor(zone.get(CONF_ZONE), sensor) - # create the hub sensor - self._upsert_sensor(HUB_ZONE, DEVICE_CLASS_TIMESTAMP) - - def _upsert_sensor(self, zone, sensor_type): - """ checks if the entity exists, and creates otherwise. always gives back the entity_id """ - sensor_id = self._get_id(zone, sensor_type) - if not (sensor_id in self._states.keys()): - zone_found = False - for existing_zone in self._zones: - # if the zone exists then a sensor is missing, - # so, get the zone and add the missing sensor - if existing_zone[CONF_ZONE] == zone: - existing_zone[CONF_SENSORS].append(sensor_type) - zone_found = True - break - if not zone_found: - # if zone does not exist, add it with the sensor and no name - self._zones.append({CONF_ZONE: zone, CONF_SENSORS: [sensor_type]}) - - # add the new sensor - sensor_name = self._get_sensor_name(zone, sensor_type) - constructor = self.sensor_types_classes.get(sensor_type) - _LOGGER.debug( - "Hub: upsert_sensor: Updating sensor: " - + sensor_name - + ", id: " - + sensor_id - + ", with constructor: " - + constructor - ) - if constructor and sensor_name: - new_sensor = eval(constructor)( - self._name, - sensor_id, - sensor_name, - sensor_type, - zone, - self._ping_interval, - self._hass, - ) - _LOGGER.debug("Hub: upsert_sensor: created sensor: " + str(new_sensor)) - self._states[sensor_id] = new_sensor - else: - _LOGGER.warning( - "Hub: Upsert Sensor: Unknown device type: %s", sensor_type - ) - return sensor_id - - def _get_id(self, zone=0, sensor_type=None): - """ Gives back a entity_id according to the variables, defaults to the hub sensor entity_id. """ - if str(zone) == "0": - return self._name + HUB_SENSOR_NAME - else: - if sensor_type: - return self._name + "_" + str(zone) + "_" + sensor_type - else: - _LOGGER.error( - "Hub: Get ID: Not allowed to create an entity_id without type, unless zone == 0." - ) - - def _get_sensor_name(self, zone=0, sensor_type=None): - """ Gives back a entity_id according to the variables, defaults to the hub sensor entity_id. """ - zone = int(zone) - if zone == 0: - return self._name + " Last heartbeat" - else: - zone_name = self._get_zone_name(zone) - if sensor_type: - return ( - self._name - + (" " + zone_name + " " if zone_name else " ") - + sensor_type - ) - else: - _LOGGER.error( - "Hub: Get Sensor Name: Not allowed to create an entity_id without type, unless zone == 0." - ) - return None + self.states = {} + self._port = int(hub_config[CONF_PORT]) + self.entry_id = entry_id + self._title = title + self._accounts = hub_config[CONF_ACCOUNTS] + self.shutdown_remove_listener = None + self._reactions = REACTIONS + + self._zones = [ + { + CONF_ACCOUNT: a[CONF_ACCOUNT], + CONF_ZONE: HUB_ZONE, + CONF_SENSORS: [DEVICE_CLASS_TIMESTAMP], + } + for a in self._accounts + ] + self._zones.extend( + [ + { + CONF_ACCOUNT: a[CONF_ACCOUNT], + CONF_ZONE: z, + CONF_SENSORS: [ + DEVICE_CLASS_ALARM, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_SMOKE, + ], + } + for a in self._accounts + for z in range(1, int(a[CONF_ZONES]) + 1) + ] + ) - def _get_zone_name(self, zone: int): - return next( - (z.get(CONF_NAME) for z in self._zones if z.get(CONF_ZONE) == zone), None + self.sia_accounts = [ + SIAAccount(a[CONF_ACCOUNT], a.get(CONF_ENCRYPTION_KEY)) + for a in self._accounts + ] + self.sia_client = SIAClient( + "", self._port, self.sia_accounts, self.update_states ) - def _update_states(self, event): - """ Updates the sensors.""" - # find the reactions for that code (if any) - reaction = self.reactions.get(event.code) - if reaction: - # get the entity_id (or create it) - sensor_id = self._upsert_sensor(event.zone, reaction["type"]) - # find out which action to take, update attribute, new state or eval for new state - attr = reaction.get("attr") - new_state = reaction.get("new_state") - new_state_eval = reaction.get("new_state_eval") - # do the work (can be more than 1) - if new_state or new_state_eval: - _LOGGER.debug( - "Hub: Update States: Will set state for entity: " - + sensor_id - + " to state: " - + (new_state if new_state else new_state_eval) - ) - self._states[sensor_id].state = ( - new_state if new_state else eval(new_state_eval) - ) - if attr: - _LOGGER.debug( - "Hub: Update States: Will set attribute entity: %s", sensor_id - ) - self._states[sensor_id].add_attribute( - { - "Last message": utcnow().isoformat() - + ": SIA: " - + event.sia_string - + ", Message: " - + event.message - } + for zone in self._zones: + ping = self._get_ping_interval(zone[CONF_ACCOUNT]) + for sensor in zone[CONF_SENSORS]: + self._create_sensor( + self._port, zone[CONF_ACCOUNT], zone[CONF_ZONE], sensor, ping ) - else: - _LOGGER.warning( - "Hub: Update States: Unhandled event type: " - + event.sia_string - + ", Message: " - + event.message - ) - # whenever a message comes in, the connection is good, so reset the availability timer for all devices. - for sensor in self._states.values(): - sensor.assume_available() - def process_event(self, event): - """Process the Event that comes from the TCP handler.""" - try: - _LOGGER.debug("Hub: Process event: %s", event) - if self._encrypted: - self._decrypt_string(event) - _LOGGER.debug("Hub: Process event, after decrypt: %s", event) - self._update_states(event) - except Exception as exc: - _LOGGER.error("Hub: Process Event: %s gave error %s", event, str(exc)) + async def async_setup_hub(self): + """Add a device to the device_registry, register shutdown listener, load reactions.""" + device_registry = await dr.async_get_registry(self._hass) + port = self._port + for acc in self._accounts: + account = acc[CONF_ACCOUNT] + device_registry.async_get_or_create( + config_entry_id=self.entry_id, + identifiers={(DOMAIN, port, account)}, + name=f"{port} - {account}", + ) + self.shutdown_remove_listener = self._hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self.async_shutdown + ) - # Even if decrypting or something else gives an error, create the acknowledgement message. - return '"ACK"{}L0#{}[{}'.format(event.sequence, self._account_id, self._ending) + async def async_shutdown(self, _: Event): + """Shutdown the SIA server.""" + await self.sia_client.stop() - def _decrypt_string(self, event): - """Decrypt the encrypted event content and parse it.""" - _LOGGER.debug("Hub: Decrypt String: Original: %s", str(event.encrypted_content)) - resmsg = self._decrypter.decrypt(unhexlify(event.encrypted_content)).decode( - encoding="UTF-8", errors="replace" + def _create_sensor( + self, port: int, account: str, zone: int, entity_type: str, ping: int + ): + """Check if the entity exists, and creates otherwise.""" + entity_id, entity_name = self._get_entity_id_and_name( + account, zone, entity_type ) - _LOGGER.debug("Hub: Decrypt String: Decrypted: %s", resmsg) - event.parse_decrypted(resmsg) + if entity_type == DEVICE_CLASS_ALARM: + new_entity = SIAAlarmControlPanel( + entity_id, entity_name, port, account, zone, ping, self._hass + ) + elif entity_type in (DEVICE_CLASS_MOISTURE, DEVICE_CLASS_SMOKE): + new_entity = SIABinarySensor( + entity_id, + entity_name, + entity_type, + port, + account, + zone, + ping, + self._hass, + ) + elif entity_type == DEVICE_CLASS_TIMESTAMP: + new_entity = SIASensor( + entity_id, + entity_name, + entity_type, + port, + account, + zone, + ping, + self._hass, + ) + self.states[entity_id] = new_entity + def _get_entity_id_and_name( + self, account: str, zone: int = 0, entity_type: str = None + ): + """Give back a entity_id and name according to the variables.""" + if zone == 0: + return ( + self._get_entity_id(account, zone, entity_type), + f"{self._port} - {account} - Last Heartbeat", + ) + if entity_type: + return ( + self._get_entity_id(account, zone, entity_type), + f"{self._port} - {account} - zone {zone} - {entity_type}", + ) + return None -class AlarmTCPHandler(socketserver.BaseRequestHandler): - """Class for the TCP Handler.""" + def _get_entity_id(self, account: str, zone: int = 0, entity_type: str = None): + """Give back a entity_id according to the variables, defaults to the hub sensor entity_id.""" + if zone == 0 or entity_type == DEVICE_CLASS_TIMESTAMP: + return f"{self._port}_{account}_{HUB_SENSOR_NAME}" + if entity_type: + return f"{self._port}_{account}_{zone}_{entity_type}" + return None - _received_data = "".encode() + def _get_ping_interval(self, account: str): + """Return the ping interval for specified account.""" + for acc in self._accounts: + if acc[CONF_ACCOUNT] == account: + return timedelta(minutes=acc[CONF_PING_INTERVAL]) + return None - def handle_line(self, line): - """Method called for each line that comes in.""" - _LOGGER.debug("TCP: Handle Line: Income raw string: %s", line) - try: - event = SIAEvent(line) - _LOGGER.debug("TCP: Handle Line: event: %s", str(event)) - if not event.valid_message: - _LOGGER.error( - "TCP: Handle Line: CRC mismatch, received: %s, calculated: %s", - event.msg_crc, - event.calc_crc, - ) - raise Exception("CRC mismatch") - if event.account not in HASS_PLATFORM.data[DOMAIN]: - _LOGGER.error( - "TCP: Handle Line: Not supported account %s", event.account - ) - raise Exception( - "TCP: Handle Line: Not supported account {}".format(event.account) - ) - response = HASS_PLATFORM.data[DOMAIN][event.account].process_event(event) - except Exception as exc: - _LOGGER.error("TCP: Handle Line: error: %s", str(exc)) - timestamp = datetime.fromtimestamp(time.time()).strftime( - "_%H:%M:%S,%m-%d-%Y" - ) - response = '"NAK"0000L0R0A0[]' + timestamp + async def update_states(self, event: SIAEvent): + """Update the sensors. This can be both a new state and a new attribute. - header = ("%04x" % len(response)).upper() - response = "\n{}{}{}\r".format( - AlarmTCPHandler.crc_calc(response), header, response - ) - byte_response = str.encode(response) - self.request.sendall(byte_response) - - def handle(self): - """Method called for handling.""" - line = b"" - try: - while True: - raw = self.request.recv(1024) - if not raw: - return - raw = bytearray(raw) - while True: - splitter = raw.find(b"\r") - if splitter > -1: - line = raw[1:splitter] - raw = raw[splitter + 1 :] - else: - break + Whenever a message comes in and is a event that should cause a reaction, the connection is good, so reset the availability timer for all devices of that account, excluding the last heartbeat. - self.handle_line(line.decode()) - except Exception as exc: - _LOGGER.error( - "TCP: Handle: last line %s gave error: %s", line.decode(), str(exc) + """ + # find the reactions for that code (if any) + reaction = self._reactions.get(event.code) + if not reaction: + _LOGGER.warning( + "Unhandled event code: %s, Message: %s, Full event: %s", + event.code, + event.message, + event.sia_string, ) return + attr = reaction.get("attr") + new_state = reaction.get("new_state") + new_state_eval = reaction.get("new_state_eval") + entity_id = self._get_entity_id( + event.account, int(event.zone), reaction["type"] + ) - @staticmethod - def crc_calc(msg): - """Calculate the CRC of the response.""" - new_crc = 0 - for letter in msg: - temp = ord(letter) - for _ in range(0, 8): - temp ^= new_crc & 1 - new_crc >>= 1 - if (temp & 1) != 0: - new_crc ^= 0xA001 - temp >>= 1 + if new_state: + self.states[entity_id].state = new_state + elif new_state_eval: + if new_state_eval == UTCNOW: + self.states[entity_id].state = utcnow() + if attr: + if attr == LAST_MESSAGE: + self.states[entity_id].add_attribute( + { + "last_message": f"{utcnow().isoformat()}: SIA: {event.sia_string}, Message: {event.message}" + } + ) - return ("%x" % new_crc).upper().zfill(4) + await asyncio.gather( + *[ + entity.assume_available() + for entity in self.states.values() + if entity.account == event.account and not isinstance(entity, SIASensor) + ] + ) diff --git a/custom_components/sia/alarm_control_panel.py b/custom_components/sia/alarm_control_panel.py index a16c07e..fac07f2 100644 --- a/custom_components/sia/alarm_control_panel.py +++ b/custom_components/sia/alarm_control_panel.py @@ -1,80 +1,93 @@ """Module for SIA Alarm Control Panels.""" import logging +from typing import Callable -from homeassistant.core import callback -from homeassistant.helpers.entity import generate_entity_id -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.components.alarm_control_panel import AlarmControlPanel -from homeassistant.util.dt import utcnow -from homeassistant.components.alarm_control_panel.const import ( - SUPPORT_ALARM_ARM_AWAY, - SUPPORT_ALARM_ARM_CUSTOM_BYPASS, - SUPPORT_ALARM_ARM_HOME, - SUPPORT_ALARM_ARM_NIGHT, - SUPPORT_ALARM_TRIGGER, +from homeassistant.components.alarm_control_panel import ( + ENTITY_ID_FORMAT as ALARM_FORMAT, + AlarmControlPanelEntity, ) -from . import ( - ALARM_FORMAT, - CONF_PING_INTERVAL, +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( CONF_ZONE, - DATA_UPDATED, - PING_INTERVAL_MARGIN, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util.dt import utcnow + +from .const import ( + CONF_ACCOUNT, + CONF_PING_INTERVAL, + DATA_UPDATED, + DOMAIN, + PING_INTERVAL_MARGIN, + PREVIOUS_STATE, +) -DOMAIN = "sia" _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Implementation of platform setup from HA.""" - devices = [ - device - for hub in hass.data[DOMAIN].values() - for device in hub._states.values() - if isinstance(device, SIAAlarmControlPanel) - ] - _LOGGER.debug("SIAAlarmControlPanel: setup: devices: " + str(devices)) - async_add_entities(devices) +async def async_setup_entry( + hass, entry: ConfigEntry, async_add_devices: Callable[[], None] +) -> bool: + """Set up sia_alarm_control_panel from a config entry.""" + async_add_devices( + [ + device + for device in hass.data[DOMAIN][entry.entry_id].states.values() + if isinstance(device, SIAAlarmControlPanel) + ] + ) + return True -class SIAAlarmControlPanel(AlarmControlPanel, RestoreEntity): + +class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity): """Class for SIA Alarm Control Panels.""" def __init__( - self, hub_name, entity_id, name, device_class, zone, ping_interval, hass + self, + entity_id: str, + name: str, + port: int, + account: str, + zone: int, + ping_interval: int, + hass: HomeAssistant, ): - _LOGGER.debug( - "SIAAlarmControlPanel: init: Initializing SIA Alarm Control Panel: " - + entity_id - ) - self._should_poll = False - self.entity_id = generate_entity_id( - entity_id_format=ALARM_FORMAT, name=entity_id, hass=hass - ) - self._unique_id = f"{hub_name}-{self.entity_id}" + """Create SIAAlarmControlPanel object.""" + self.entity_id = ALARM_FORMAT.format(entity_id) + self._unique_id = entity_id self._name = name - self.hass = hass + self._port = port + self._account = account + self._zone = zone self._ping_interval = ping_interval - self._attr = {CONF_PING_INTERVAL: self.ping_interval, CONF_ZONE: zone} + self.hass = hass + + self._should_poll = False self._is_available = True self._remove_unavailability_tracker = None self._state = None + self._old_state = None + self._attr = { + CONF_ACCOUNT: self._account, + CONF_PING_INTERVAL: str(self._ping_interval), + CONF_ZONE: self._zone, + } async def async_added_to_hass(self): """Once the panel is added, see if it was there before and pull in that state.""" - _LOGGER.debug("SIAAlarmControlPanel: init: added_to_hass") await super().async_added_to_hass() state = await self.async_get_last_state() if state is not None and state.state is not None: - _LOGGER.debug("SIAAlarmControlPanel: init: old state: " + state.state) if state.state == STATE_ALARM_ARMED_AWAY: self.state = STATE_ALARM_ARMED_AWAY elif state.state == STATE_ALARM_ARMED_NIGHT: @@ -88,11 +101,8 @@ async def async_added_to_hass(self): else: self.state = None else: - _LOGGER.debug("SIAAlarmControlPanel: no previous state.") - return - # self.state = STATE_ALARM_DISARMED # assume disarmed - _LOGGER.debug("SIAAlarmControlPanel: added: state: " + str(state)) - self._async_track_unavailable() + self.state = None + await self._async_track_unavailable() async_dispatcher_connect( self.hass, DATA_UPDATED, self._schedule_immediate_update ) @@ -102,70 +112,55 @@ def _schedule_immediate_update(self): self.async_schedule_update_ha_state(True) @property - def name(self): + def name(self) -> str: """Get Name.""" return self._name @property - def ping_interval(self): + def ping_interval(self) -> int: """Get ping_interval.""" return str(self._ping_interval) @property - def state(self): + def state(self) -> str: """Get state.""" return self._state + @property + def account(self) -> str: + """Return device account.""" + return self._account + @property def unique_id(self) -> str: """Get unique_id.""" return self._unique_id @property - def available(self): + def available(self) -> bool: """Get availability.""" return self._is_available - def alarm_disarm(self, code=None): - """Method for disarming, not implemented.""" - _LOGGER.debug("Not implemented.") - - def alarm_arm_home(self, code=None): - """Method for arming, not implemented.""" - _LOGGER.debug("Not implemented.") - - def alarm_arm_away(self, code=None): - """Method for arming, not implemented.""" - _LOGGER.debug("Not implemented.") - - def alarm_arm_night(self, code=None): - """Method for arming, not implemented.""" - _LOGGER.debug("Not implemented.") - - def alarm_trigger(self, code=None): - """Method for triggering, not implemented.""" - _LOGGER.debug("Not implemented.") - - def alarm_arm_custom_bypass(self, code=None): - """Method for arming, not implemented.""" - _LOGGER.debug("Not implemented.") - @property - def device_state_attributes(self): + def device_state_attributes(self) -> dict: + """Return device attributes.""" return self._attr @state.setter - def state(self, state): - self._state = state + def state(self, state: str): + """Set state.""" + temp = self._old_state if state == PREVIOUS_STATE else state + self._old_state = self._state + self._state = temp self.async_schedule_update_ha_state() - def assume_available(self): + async def assume_available(self): """Reset unavalability tracker.""" - self._async_track_unavailable() + await self._async_track_unavailable() @callback - def _async_track_unavailable(self): - """Callback method for resetting unavailability.""" + async def _async_track_unavailable(self) -> bool: + """Reset unavailability.""" if self._remove_unavailability_tracker: self._remove_unavailability_tracker() self._remove_unavailability_tracker = async_track_point_in_utc_time( @@ -179,7 +174,8 @@ def _async_track_unavailable(self): return False @callback - def _async_set_unavailable(self, now): + def _async_set_unavailable(self, _): + """Set availability.""" self._remove_unavailability_tracker = None self._is_available = False self.async_schedule_update_ha_state() @@ -187,10 +183,13 @@ def _async_set_unavailable(self, now): @property def supported_features(self) -> int: """Return the list of supported features.""" - return ( - SUPPORT_ALARM_ARM_AWAY - | SUPPORT_ALARM_ARM_CUSTOM_BYPASS - | SUPPORT_ALARM_ARM_HOME - | SUPPORT_ALARM_ARM_NIGHT - | SUPPORT_ALARM_TRIGGER - ) + return None + + @property + def device_info(self) -> dict: + """Return the device_info.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "via_device": (DOMAIN, self._port, self._account), + } diff --git a/custom_components/sia/binary_sensor.py b/custom_components/sia/binary_sensor.py index d8db2eb..ed4bd37 100644 --- a/custom_components/sia/binary_sensor.py +++ b/custom_components/sia/binary_sensor.py @@ -1,122 +1,161 @@ """Module for SIA Binary Sensors.""" import logging +from typing import Callable -from homeassistant.core import callback -from homeassistant.helpers.entity import generate_entity_id +from homeassistant.components.binary_sensor import ( + ENTITY_ID_FORMAT as BINARY_SENSOR_FORMAT, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ZONE, STATE_OFF, STATE_ON, STATE_UNKNOWN +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util.dt import utcnow -from . import ( +from .const import ( + CONF_ACCOUNT, CONF_PING_INTERVAL, - PING_INTERVAL_MARGIN, - CONF_ZONE, DATA_UPDATED, - BINARY_SENSOR_FORMAT, - STATE_ON, - STATE_OFF, + DOMAIN, + PING_INTERVAL_MARGIN, ) -DOMAIN = "sia" _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Implementation of platform setup from HA.""" - devices = [ - device - for hub in hass.data[DOMAIN].values() - for device in hub._states.values() - if isinstance(device, SIABinarySensor) - ] - _LOGGER.debug("SIABinarySensor: setup: devices: " + str(devices)) - async_add_entities(devices) +async def async_setup_entry( + hass, entry: ConfigEntry, async_add_devices: Callable[[], None] +) -> bool: + """Set up sia_binary_sensor from a config entry.""" + async_add_devices( + [ + device + for device in hass.data[DOMAIN][entry.entry_id].states.values() + if isinstance(device, SIABinarySensor) + ] + ) + return True -class SIABinarySensor(RestoreEntity): + +class SIABinarySensor(BinarySensorEntity, RestoreEntity): """Class for SIA Binary Sensors.""" def __init__( - self, hub_name, entity_id, name, device_class, zone, ping_interval, hass + self, + entity_id: str, + name: str, + device_class: str, + port: int, + account: str, + zone: int, + ping_interval: int, + hass: HomeAssistant, ): + """Create SIABinarySensor object.""" + + self.entity_id = BINARY_SENSOR_FORMAT.format(entity_id) + self._unique_id = entity_id + self._name = name self._device_class = device_class - self._should_poll = False + self._port = port + self._account = account + self._zone = zone self._ping_interval = ping_interval - self._attr = {CONF_PING_INTERVAL: self.ping_interval, CONF_ZONE: zone} - self.entity_id = generate_entity_id( - entity_id_format=BINARY_SENSOR_FORMAT, name=entity_id, hass=hass - ) - self._unique_id = f"{hub_name}-{self.entity_id}" - self._name = name self.hass = hass + + self._should_poll = False + self._is_on = None self._is_available = True self._remove_unavailability_tracker = None - self._state = None + self._attr = { + CONF_ACCOUNT: self._account, + CONF_PING_INTERVAL: str(self._ping_interval), + CONF_ZONE: self._zone, + } async def async_added_to_hass(self): + """Add sensor to HASS.""" await super().async_added_to_hass() state = await self.async_get_last_state() if state is not None and state.state is not None: - self.state = state.state == STATE_ON - else: - self.state = None - _LOGGER.debug("SIABinarySensor: added: state: " + str(state)) - self._async_track_unavailable() + if state.state == STATE_ON: + self._is_on = True + elif state.state == STATE_OFF: + self._is_on = False + await self._async_track_unavailable() async_dispatcher_connect( self.hass, DATA_UPDATED, self._schedule_immediate_update ) @callback def _schedule_immediate_update(self): + """Schedule update.""" self.async_schedule_update_ha_state(True) @property - def name(self): + def name(self) -> str: + """Return name.""" return self._name @property - def ping_interval(self): + def ping_interval(self) -> int: """Get ping_interval.""" return str(self._ping_interval) - @property - def state(self): - return STATE_ON if self.is_on else STATE_OFF - @property def unique_id(self) -> str: + """Return unique id.""" return self._unique_id @property - def available(self): + def account(self) -> str: + """Return device account.""" + return self._account + + @property + def available(self) -> bool: + """Return avalability.""" return self._is_available @property - def device_state_attributes(self): + def device_state_attributes(self) -> dict: + """Return attributes.""" return self._attr @property - def device_class(self): + def device_class(self) -> str: + """Return device class.""" return self._device_class @property - def is_on(self): - """Get whether the sensor is set to ON.""" - return self._state + def state(self) -> str: + """Return the state of the binary sensor.""" + if self.is_on is None: + return STATE_UNKNOWN + return STATE_ON if self.is_on else STATE_OFF + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self._is_on @state.setter - def state(self, state): - self._state = state + def state(self, new_on: bool): + """Set state.""" + self._is_on = new_on self.async_schedule_update_ha_state() - def assume_available(self): + async def assume_available(self): """Reset unavalability tracker.""" - self._async_track_unavailable() + await self._async_track_unavailable() @callback - def _async_track_unavailable(self): + async def _async_track_unavailable(self) -> bool: + """Track availability.""" if self._remove_unavailability_tracker: self._remove_unavailability_tracker() self._remove_unavailability_tracker = async_track_point_in_utc_time( @@ -131,6 +170,16 @@ def _async_track_unavailable(self): @callback def _async_set_unavailable(self, now): + """Set unavailable.""" self._remove_unavailability_tracker = None self._is_available = False self.async_schedule_update_ha_state() + + @property + def device_info(self) -> dict: + """Return the device_info.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "via_device": (DOMAIN, self._port, self._account), + } diff --git a/custom_components/sia/config_flow.py b/custom_components/sia/config_flow.py new file mode 100644 index 0000000..4398c01 --- /dev/null +++ b/custom_components/sia/config_flow.py @@ -0,0 +1,166 @@ +"""Config flow for sia integration.""" +import logging + +from pysiaalarm import ( + InvalidAccountFormatError, + InvalidAccountLengthError, + InvalidKeyFormatError, + InvalidKeyLengthError, + SIAAccount, +) +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_PORT +from homeassistant.data_entry_flow import AbortFlow + +from .const import ( + CONF_ACCOUNT, + CONF_ACCOUNTS, + CONF_ADDITIONAL_ACCOUNTS, + CONF_ENCRYPTION_KEY, + CONF_PING_INTERVAL, + CONF_ZONES, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +HUB_SCHEMA = vol.Schema( + { + vol.Required(CONF_PORT): int, + vol.Required(CONF_ACCOUNT): str, + vol.Optional(CONF_ENCRYPTION_KEY): str, + vol.Required(CONF_PING_INTERVAL, default=1): int, + vol.Required(CONF_ZONES, default=1): int, + vol.Optional(CONF_ADDITIONAL_ACCOUNTS, default=False): bool, + } +) + +ACCOUNT_SCHEMA = vol.Schema( + { + vol.Required(CONF_ACCOUNT): str, + vol.Optional(CONF_ENCRYPTION_KEY): str, + vol.Required(CONF_PING_INTERVAL, default=1): int, + vol.Required(CONF_ZONES, default=1): int, + vol.Optional(CONF_ADDITIONAL_ACCOUNTS, default=False): bool, + } +) + + +def validate_input(data: dict) -> bool: + """Validate the input by the user.""" + SIAAccount(data[CONF_ACCOUNT], data.get(CONF_ENCRYPTION_KEY)) + + try: + ping = int(data[CONF_PING_INTERVAL]) + assert 1 <= ping <= 1440 + except AssertionError: + raise InvalidPing + try: + zones = int(data[CONF_ZONES]) + assert zones > 0 + except AssertionError: + raise InvalidZones + + return True + + +class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for sia.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + data = None + + async def async_step_add_account(self, user_input: dict = None): + """Handle the additional accounts steps.""" + errors = {} + if user_input is not None: + try: + if validate_input(user_input): + add_data = user_input.copy() + add_data.pop(CONF_ADDITIONAL_ACCOUNTS) + self.data[CONF_ACCOUNTS].append(add_data) + if user_input[CONF_ADDITIONAL_ACCOUNTS]: + return await self.async_step_add_account() + except InvalidKeyFormatError: + errors["base"] = "invalid_key_format" + except InvalidKeyLengthError: + errors["base"] = "invalid_key_length" + except InvalidAccountFormatError: + errors["base"] = "invalid_account_format" + except InvalidAccountLengthError: + errors["base"] = "invalid_account_length" + except InvalidPing: + errors["base"] = "invalid_ping" + except InvalidZones: + errors["base"] = "invalid_zones" + + return self.async_show_form( + step_id="user", data_schema=ACCOUNT_SCHEMA, errors=errors, + ) + + async def async_step_user(self, user_input: dict = None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + if validate_input(user_input): + if not self.data: + self.data = { + CONF_PORT: user_input[CONF_PORT], + CONF_ACCOUNTS: [ + { + CONF_ACCOUNT: user_input[CONF_ACCOUNT], + CONF_ENCRYPTION_KEY: user_input.get( + CONF_ENCRYPTION_KEY + ), + CONF_PING_INTERVAL: user_input[CONF_PING_INTERVAL], + CONF_ZONES: user_input[CONF_ZONES], + } + ], + } + else: + add_data = user_input.copy() + add_data.pop(CONF_ADDITIONAL_ACCOUNTS) + self.data[CONF_ACCOUNTS].append(add_data) + await self.async_set_unique_id(f"{DOMAIN}_{self.data[CONF_PORT]}") + self._abort_if_unique_id_configured() + + if not user_input[CONF_ADDITIONAL_ACCOUNTS]: + return self.async_create_entry( + title=f"SIA Alarm on port {self.data[CONF_PORT]}", + data=self.data, + ) + return await self.async_step_add_account() + except InvalidKeyFormatError: + errors["base"] = "invalid_key_format" + except InvalidKeyLengthError: + errors["base"] = "invalid_key_length" + except InvalidAccountFormatError: + errors["base"] = "invalid_account_format" + except InvalidAccountLengthError: + errors["base"] = "invalid_account_length" + except InvalidPing: + errors["base"] = "invalid_ping" + except InvalidZones: + errors["base"] = "invalid_zones" + except AbortFlow: + return self.async_abort(reason="already_configured") + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=HUB_SCHEMA, errors=errors + ) + + +class InvalidPing(exceptions.HomeAssistantError): + """Error to indicate there is invalid ping interval.""" + + +class InvalidZones(exceptions.HomeAssistantError): + """Error to indicate there is invalid number of zones.""" diff --git a/custom_components/sia/const.py b/custom_components/sia/const.py new file mode 100644 index 0000000..6a1d701 --- /dev/null +++ b/custom_components/sia/const.py @@ -0,0 +1,52 @@ +"""Constants for the sia integration.""" + +from datetime import timedelta + +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, +) +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN + +CONF_ACCOUNT = "account" +CONF_ACCOUNTS = "accounts" +CONF_ADDITIONAL_ACCOUNTS = "additional_account" +CONF_PING_INTERVAL = "ping_interval" +CONF_ENCRYPTION_KEY = "encryption_key" +CONF_ZONES = "zones" +DOMAIN = "sia" +DATA_UPDATED = f"{DOMAIN}_data_updated" +DEFAULT_NAME = "SIA Alarm" +DEVICE_CLASS_ALARM = "alarm" +HUB_SENSOR_NAME = "last_heartbeat" +HUB_ZONE = 0 +PING_INTERVAL_MARGIN = timedelta(seconds=30) +PREVIOUS_STATE = "previous_state" +UTCNOW = "utcnow" +LAST_MESSAGE = "last_message" + +PLATFORMS = [SENSOR_DOMAIN, BINARY_SENSOR_DOMAIN, ALARM_CONTROL_PANEL_DOMAIN] + +REACTIONS = { + "BA": {"type": "alarm", "new_state": "triggered"}, + "BR": {"type": "alarm", "new_state": "previous_state"}, + "CA": {"type": "alarm", "new_state": "armed_away"}, + "CF": {"type": "alarm", "new_state": "armed_custom_bypass"}, + "CG": {"type": "alarm", "new_state": "armed_away"}, + "CL": {"type": "alarm", "new_state": "armed_away"}, + "CP": {"type": "alarm", "new_state": "armed_away"}, + "CQ": {"type": "alarm", "new_state": "armed_away"}, + "GA": {"type": "smoke", "new_state": True}, + "GH": {"type": "smoke", "new_state": False}, + "NL": {"type": "alarm", "new_state": "armed_night"}, + "OA": {"type": "alarm", "new_state": "disarmed"}, + "OG": {"type": "alarm", "new_state": "disarmed"}, + "OP": {"type": "alarm", "new_state": "disarmed"}, + "OQ": {"type": "alarm", "new_state": "disarmed"}, + "OR": {"type": "alarm", "new_state": "disarmed"}, + "RP": {"type": "timestamp", "new_state_eval": "utcnow"}, + "TA": {"type": "alarm", "new_state": "triggered"}, + "WA": {"type": "moisture", "new_state": True}, + "WH": {"type": "moisture", "new_state": False}, + "YG": {"type": "timestamp", "attr": "last_message"}, +} diff --git a/custom_components/sia/manifest.json b/custom_components/sia/manifest.json index f9dd24a..090d1a0 100644 --- a/custom_components/sia/manifest.json +++ b/custom_components/sia/manifest.json @@ -1,8 +1,8 @@ { "domain": "sia", - "name": "Sia", - "documentation": "", - "dependencies": [], - "codeowners": ["@cheater.dev", "@eavanvalkenburg"], - "requirements": [] -} \ No newline at end of file + "name": "SIA Alarm Systems", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sia", + "requirements": ["pysiaalarm==2.0.3"], + "codeowners": ["@eavanvalkenburg"] +} diff --git a/custom_components/sia/reactions.json b/custom_components/sia/reactions.json new file mode 100644 index 0000000..1d2b071 --- /dev/null +++ b/custom_components/sia/reactions.json @@ -0,0 +1,29 @@ +{ + "BA": { "type": "alarm", "new_state": "triggered" }, + "BR": { "type": "alarm", "new_state": "previous_state" }, + "CA": { "type": "alarm", "new_state": "armed_away" }, + "CF": { + "type": "alarm", + "new_state": "armed_custom_bypass" + }, + "CG": { "type": "alarm", "new_state": "armed_away" }, + "CL": { "type": "alarm", "new_state": "armed_away" }, + "CP": { "type": "alarm", "new_state": "armed_away" }, + "CQ": { "type": "alarm", "new_state": "armed_away" }, + "GA": { "type": "smoke", "new_state": true }, + "GH": { "type": "smoke", "new_state": false }, + "NL": { + "type": "alarm", + "new_state": "armed_night" + }, + "OA": { "type": "alarm", "new_state": "disarmed" }, + "OG": { "type": "alarm", "new_state": "disarmed" }, + "OP": { "type": "alarm", "new_state": "disarmed" }, + "OQ": { "type": "alarm", "new_state": "disarmed" }, + "OR": { "type": "alarm", "new_state": "disarmed" }, + "RP": { "type": "timestamp", "new_state_eval": "utcnow" }, + "TA": { "type": "alarm", "new_state": "triggered" }, + "WA": { "type": "moisture", "new_state": true }, + "WH": { "type": "moisture", "new_state": false }, + "YG": { "type": "timestamp", "attr": "last_message" } +} diff --git a/custom_components/sia/sensor.py b/custom_components/sia/sensor.py index 866a1f6..a56877a 100644 --- a/custom_components/sia/sensor.py +++ b/custom_components/sia/sensor.py @@ -1,70 +1,90 @@ """Module for SIA Sensors.""" -import logging import datetime as dt +import logging +from typing import Callable -from homeassistant.core import callback from homeassistant.components.sensor import ENTITY_ID_FORMAT as SENSOR_FORMAT +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ZONE +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity, generate_entity_id from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util.dt import utcnow -from . import CONF_ZONE, CONF_PING_INTERVAL, DATA_UPDATED +from .const import CONF_ACCOUNT, CONF_PING_INTERVAL, DATA_UPDATED, DOMAIN -DOMAIN = "sia" _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Implementation of platform setup from HA.""" - devices = [ - device - for hub in hass.data[DOMAIN].values() - for device in hub._states.values() - if isinstance(device, SIASensor) - ] - _LOGGER.debug("SIASensor: setup: devices: " + str(devices)) - async_add_entities(devices) +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_devices: Callable[[], None] +) -> bool: + """Set up sia_sensor from a config entry.""" + async_add_devices( + [ + device + for device in hass.data[DOMAIN][entry.entry_id].states.values() + if isinstance(device, SIASensor) + ] + ) + + return True class SIASensor(RestoreEntity): """Class for SIA Sensors.""" def __init__( - self, hub_name, entity_id, name, device_class, zone, ping_interval, hass + self, + entity_id: str, + name: str, + device_class: str, + port: int, + account: str, + zone: int, + ping_interval: int, + hass: HomeAssistant, ): - self._should_poll = False - self._device_class = device_class - self.entity_id = generate_entity_id( - entity_id_format=SENSOR_FORMAT, name=entity_id, hass=hass - ) - self._unique_id = f"{hub_name}-{self.entity_id}" - self._state = utcnow() - self._attr = {CONF_PING_INTERVAL: str(ping_interval), CONF_ZONE: zone} + """Create SIASensor object.""" + self.entity_id = SENSOR_FORMAT.format(entity_id) + self._unique_id = entity_id self._name = name + self._device_class = device_class + self._port = port + self._account = account + self._zone = zone + self._ping_interval = str(ping_interval) self.hass = hass + self._state = utcnow() + self._should_poll = False + self._attr = { + CONF_ACCOUNT: self._account, + CONF_PING_INTERVAL: self._ping_interval, + CONF_ZONE: self._zone, + } + async def async_added_to_hass(self): """Once the sensor is added, see if it was there before and pull in that state.""" await super().async_added_to_hass() state = await self.async_get_last_state() if state is not None and state.state is not None: - _LOGGER.debug("SIASensor: init: old state: " + state.state) self.state = dt.datetime.strptime(state.state, "%Y-%m-%dT%H:%M:%S.%f%z") else: return - _LOGGER.debug("SIASensor: added: state: " + str(state)) async_dispatcher_connect( self.hass, DATA_UPDATED, self._schedule_immediate_update ) @callback def _schedule_immediate_update(self): + """Schedule update.""" self.async_schedule_update_ha_state(True) @property - def name(self): + def name(self) -> str: + """Return name.""" return self._name @property @@ -73,31 +93,50 @@ def unique_id(self) -> str: return self._unique_id @property - def state(self): + def state(self) -> str: + """Return state.""" return self._state.isoformat() @property - def device_state_attributes(self): + def account(self) -> str: + """Return device account.""" + return self._account + + @property + def device_state_attributes(self) -> dict: + """Return attributes.""" return self._attr - def add_attribute(self, attr): + def add_attribute(self, attr: dict): """Update attributes.""" self._attr.update(attr) @property - def device_class(self): + def device_class(self) -> str: + """Return device class.""" return self._device_class @state.setter - def state(self, state): + def state(self, state: dt.datetime): + """Set state.""" self._state = state self.async_schedule_update_ha_state() - def assume_available(self): - """Stub method, to keep signature the same between all SIA components.""" - pass - @property - def icon(self): + def icon(self) -> str: """Return the icon to use in the frontend, if any.""" return "mdi:alarm-light-outline" + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement.""" + return "ISO8601" + + @property + def device_info(self) -> dict: + """Return the device_info.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "via_device": (DOMAIN, self._port, self._account), + } diff --git a/custom_components/sia/sia_event.py b/custom_components/sia/sia_event.py deleted file mode 100644 index 689603d..0000000 --- a/custom_components/sia/sia_event.py +++ /dev/null @@ -1,1935 +0,0 @@ -"""Module for SIA Events.""" - -import re - -from . import _LOGGER - - -class SIAEvent: - """Class for SIA Events.""" - - def __init__(self, line): - # Example events: 98100078"*SIA-DCS"5994L0#acct[5AB718E008C616BF16F6468033A11326B0F7546CAB230910BCA10E4DEBA42283C436E4F8EFF50931070DDE36D5BB5F0C - # Example events: 66100078"*SIA-DCS"6001L0#acct[6F7457178C6F0EAD99109E1DC5B75B26EDFBE1AA17361CD48E0B0E340081035F16AD2A25CD3D7F04105EC1EA65BF6341 - # Example events: 2E680078"*SIA-DCS"6002L0#acct[FDDCDFEC950EDC3F7C438B75CD57B9C91E1CA632806882769097C60292F86BD13D43D3BA7E2F529560DC7B51E6581E58 - # Example events: 2E680078"SIA-DCS"6002L0#acct[|Nri1/CL501]_14:12:04,09-25-2019 - # Example events: 5BFD0078"*SIA-DCS"6003L0#acct[03D1EA959BCC9E2DA91CACA7AFF472F1CB234708977C4E1E3B86A8ABD45AD9F95F0EFFFF817EE5349572972325BFC856 - # Example events: 5BFD0078"SIA-DCS"6003L0#acct[|Nri1/OP501]_14:12:04,09-25-2019 - - regex = r"(.{4})0[A-F0-9]{3}(\"(SIA-DCS|\*SIA-DCS)\"([0-9]{4})(R[A-F0-9]{1,6})?(L[A-F0-9]{1,6})#([A-F0-9]{3,16})\[([A-F0-9]*)?(.*Nri(\d*)/([a-zA-z]{2})(.*)]_([0-9:,-]*))?)" - matches = re.findall(regex, line) - - # check if there is at lease one match - if not matches: - raise ValueError("SIAEvent: Constructor: no matches found.") - # _LOGGER.debug(matches) - self.msg_crc, self.full_message, self.message_type, self.sequence, self.receiver, self.prefix, self.account, self.encrypted_content, self.content, self.zone, self.code, self.message, self.timestamp = matches[ - 0 - ] - self.type = "" - self.description = "" - self.concerns = "" - self.calc_crc = SIAEvent.crc_calc(self.full_message) - if self.code: - self._add_sia() - - def _add_sia(self): - """Finds the sia codes based on self.code.""" - full = self.all_codes.get(self.code, None) - if full: - self.type = full.get("type") - self.description = full.get("description") - self.concerns = full.get("concerns") - else: - raise LookupError("Code not found: {}".format(self.code)) - - def parse_decrypted(self, new_data): - """When the content was decrypted, update the fields contained within.""" - regex = r".*Nri(\d*)/([a-zA-z]{2})(.*)]_([0-9:,-]*)" - matches = re.findall(regex, new_data) - if not matches: - raise ValueError("SIAEvent: Parse Decrypted: no matches found.") - self.zone, self.code, self.message, self.timestamp = matches[0] - if self.code: - self._add_sia() - - @staticmethod - def crc_calc(msg): - """Calculate the CRC of the events.""" - crc = 0 - for letter in str.encode(msg): - temp = letter - for _ in range(0, 8): - temp ^= crc & 1 - crc >>= 1 - if (temp & 1) != 0: - crc ^= 0xA001 - temp >>= 1 - return ("%x" % crc).upper().zfill(4) - - @property - def valid_message(self): - """Check the validity of the message by comparing the sent CRC with the calculated CRC.""" - return self.msg_crc == self.calc_crc - - @property - def sia_string(self): - """Create a string with the SIA codes and some other fields.""" - return "Code: {}, Type: {}, Description: {}, Concerns: {}".format( - self.code, self.type, self.description, self.concerns - ) - - def __str__(self): - return "CRC: {}, Calc CRC: {}, Full Message: {}, Message type: {}, Sequence: {}, Receiver: {}, Prefix: {}, Account: {}, Encrypted Content: {}, Content: {}, Zone: {}, Code: {}, Message: {}, Timestamp: {}, Code: {}, Type: {}, Description: {}, Concerns: {}".format( - self.msg_crc, - self.calc_crc, - self.full_message, - self.message_type, - self.sequence, - self.receiver, - self.prefix, - self.account, - self.encrypted_content, - self.content, - self.zone, - self.code, - self.message, - self.timestamp, - self.code, - self.type, - self.description, - self.concerns, - ) - - all_codes = { - "AA": { - "code": "AA", - "type": "Alarm – Panel Substitution", - "description": "An attempt to substitute an alternate alarm panel for a secure panel has been made", - "concerns": "Condition number", - }, - "AB": { - "code": "AB", - "type": "Abort", - "description": "An event message was not sent due to User action", - "concerns": "Zone or point", - }, - "AN": { - "code": "AN", - "type": "Analog Restoral", - "description": "An analog fire sensor has been restored to normal operation", - "concerns": "Zone or point", - }, - "AR": { - "code": "AR", - "type": "AC Restoral", - "description": "AC power has been restored", - "concerns": "Unused", - }, - "AS": { - "code": "AS", - "type": "Analog Service", - "description": "An analog fire sensor needs to be cleaned or calibrated", - "concerns": "Zone or point", - }, - "AT": { - "code": "AT", - "type": "AC Trouble", - "description": "AC power has been failed", - "concerns": "Unused", - }, - "BA": { - "code": "BA", - "type": "Burglary Alarm", - "description": "Burglary zone has been violated while armed", - "concerns": "Zone or point", - }, - "BB": { - "code": "BB", - "type": "Burglary Bypass", - "description": "Burglary zone has been bypassed", - "concerns": "Zone or point", - }, - "BC": { - "code": "BC", - "type": "Burglary Cancel", - "description": "Alarm has been cancelled by authorized user", - "concerns": "User number", - }, - "BD": { - "code": "BD", - "type": "Swinger Trouble", - "description": "A non-fire zone has been violated after a Swinger Shutdown on the zone", - "concerns": "Zone or point", - }, - "BE": { - "code": "BE", - "type": "Swinger Trouble Restore", - "description": "A non-fire zone restores to normal from a Swinger Trouble state", - "concerns": "Zone or point", - }, - "BG": { - "code": "BG", - "type": "Unverified Event - Burglary", - "description": "A point assigned to a Cross Point group has gone into alarm but the Cross Point remained normal", - "concerns": "Zone or point", - }, - "BH": { - "code": "BH", - "type": "Burglary Alarm Restore", - "description": "Alarm condition eliminated", - "concerns": "Zone or point", - }, - "BJ": { - "code": "BJ", - "type": "Burglary Trouble Restore", - "description": "Trouble condition eliminated", - "concerns": "Zone or point", - }, - "BM": { - "code": "BM", - "type": "Burglary Alarm - Cross Point", - "description": "Burglary alarm w/cross point also in alarm - alarm verified", - "concerns": "Zone or point", - }, - "BR": { - "code": "BR", - "type": "Burglary Restoral", - "description": "Alarm/trouble condition has been eliminated", - "concerns": "Zone or point", - }, - "BS": { - "code": "BS", - "type": "Burglary Supervisory", - "description": "Unsafe intrusion detection system condition", - "concerns": "Zone or point", - }, - "BT": { - "code": "BT", - "type": "Burglary Trouble", - "description": "Burglary zone disabled by fault", - "concerns": "Zone or point", - }, - "BU": { - "code": "BU", - "type": "Burglary Unbypass", - "description": "Zone bypass has been removed", - "concerns": "Zone or point", - }, - "BV": { - "code": "BV", - "type": "Burglary Verified", - "description": "A burglary alarm has occurred and been verified within programmed conditions. (zone or point not sent)", - "concerns": "Area number", - }, - "BX": { - "code": "BX", - "type": "Burglary Test", - "description": "Burglary zone activated during testing", - "concerns": "Zone or point", - }, - "BZ": { - "code": "BZ", - "type": "Missing Supervision", - "description": "A non-fire Supervisory point has gone missing", - "concerns": "Zone or point", - }, - "CA": { - "code": "CA", - "type": "Automatic Closing", - "description": "System armed automatically", - "concerns": "Area number", - }, - "CD": { - "code": "CD", - "type": "Closing Delinquent", - "description": "The system has not been armed for a programmed amount of time", - "concerns": "Area number", - }, - "CE": { - "code": "CE", - "type": "Closing Extend", - "description": "Extend closing time", - "concerns": "User number", - }, - "CF": { - "code": "CF", - "type": "Forced Closing", - "description": "System armed, some zones not ready", - "concerns": "User number", - }, - "CG": { - "code": "CG", - "type": "Close Area", - "description": "System has been partially armed", - "concerns": "Area number", - }, - "CI": { - "code": "CI", - "type": "Fail to Close", - "description": "An area has not been armed at the end of the closing window", - "concerns": "Area number", - }, - "CJ": { - "code": "CJ", - "type": "Late Close", - "description": "An area was armed after the closing window", - "concerns": "User number", - }, - "CK": { - "code": "CK", - "type": "Early Close", - "description": "An area was armed before the closing window", - "concerns": "User number", - }, - "CL": { - "code": "CL", - "type": "Closing Report", - "description": "System armed, normal", - "concerns": "User number", - }, - "CM": { - "code": "CM", - "type": "Missing Alarm - Recent Closing", - "description": "A point has gone missing within 2 minutes of closing", - "concerns": "Zone or point", - }, - "CO": { - "code": "CO", - "type": "Command Sent", - "description": "A command has been sent to an expansion/peripheral device", - "concerns": "Condition number", - }, - "CP": { - "code": "CP", - "type": "Automatic Closing", - "description": "System armed automatically", - "concerns": "User number", - }, - "CQ": { - "code": "CQ", - "type": "Remote Closing", - "description": "The system was armed from a remote location", - "concerns": "User number", - }, - "CR": { - "code": "CR", - "type": "Recent Closing", - "description": "An alarm occurred within five minutes after the system was closed", - "concerns": "User number", - }, - "CS": { - "code": "CS", - "type": "Closing Keyswitch", - "description": "Account has been armed by keyswitch", - "concerns": "Zone or point", - }, - "CT": { - "code": "CT", - "type": "Late to Open", - "description": "System was not disarmed on time", - "concerns": "Area number", - }, - "CW": { - "code": "CW", - "type": "Was Force Armed", - "description": "Header for a force armed session, forced point msgs may follow", - "concerns": "Area number", - }, - "CX": { - "code": "CX", - "type": "Custom Function Executed", - "description": "The panel has executed a preprogrammed set of instructions", - "concerns": "Custom Function number", - }, - "CZ": { - "code": "CZ", - "type": "Point Closing", - "description": "A point, as opposed to a whole area or account, has closed", - "concerns": "Zone or point", - }, - "DA": { - "code": "DA", - "type": "Card Assigned", - "description": "An access ID has been added to the controller", - "concerns": "User number", - }, - "DB": { - "code": "DB", - "type": "Card Deleted", - "description": "An access ID has been deleted from the controller", - "concerns": "User number", - }, - "DC": { - "code": "DC", - "type": "Access Closed", - "description": "Access to all users prohibited", - "concerns": "Door number", - }, - "DD": { - "code": "DD", - "type": "Access Denied", - "description": "Access denied, unknown code", - "concerns": "Door number", - }, - "DE": { - "code": "DE", - "type": "Request to Enter", - "description": "An access point was opened via a Request to Enter device", - "concerns": "Door number", - }, - "DF": { - "code": "DF", - "type": "Door Forced", - "description": "Door opened without access request", - "concerns": "Door number", - }, - "DG": { - "code": "DG", - "type": "Access Granted", - "description": "Door access granted", - "concerns": "Door number", - }, - "DH": { - "code": "DH", - "type": "Door Left Open - Restoral", - "description": "An access point in a Door Left Open state has restored", - "concerns": "Door number", - }, - "DI": { - "code": "DI", - "type": "Access Denied – Passback", - "description": "Access denied because credential has not exited area before attempting to re-enter same area", - "concerns": "Door number", - }, - "DJ": { - "code": "DJ", - "type": "Door Forced - Trouble", - "description": "An access point has been forced open in an unarmed area", - "concerns": "Door number", - }, - "DK": { - "code": "DK", - "type": "Access Lockout", - "description": "Access denied, known code", - "concerns": "Door number", - }, - "DL": { - "code": "DL", - "type": "Door Left Open - Alarm", - "description": "An open access point when open time expired in an armed area", - "concerns": "Door number", - }, - "DM": { - "code": "DM", - "type": "Door Left Open - Trouble", - "description": "An open access point when open time expired in an unarmed area", - "concerns": "Door number", - }, - "DN": { - "code": "DN", - "type": "Door Left Open (non-alarm, non-trouble)", - "description": "An access point was open when the door cycle time expired", - "concerns": "Door number", - }, - "DO": { - "code": "DO", - "type": "Access Open", - "description": "Access to authorized users allowed", - "concerns": "Door number", - }, - "DP": { - "code": "DP", - "type": "Access Denied - Unauthorized Time", - "description": "An access request was denied because the request is occurring outside the user’s authorized time window(s)", - "concerns": "Door number", - }, - "DQ": { - "code": "DQ", - "type": "Access Denied - Unauthorized Arming State", - "description": "An access request was denied because the user was not authorized in this area when the area was armed", - "concerns": "Door number", - }, - "DR": { - "code": "DR", - "type": "Door Restoral", - "description": "Access alarm/trouble condition eliminated", - "concerns": "Door number", - }, - "DS": { - "code": "DS", - "type": "Door Station", - "description": "Identifies door for next report", - "concerns": "Door number", - }, - "DT": { - "code": "DT", - "type": "Access Trouble", - "description": "Access system trouble", - "concerns": "Unused", - }, - "DU": { - "code": "DU", - "type": "Dealer ID", - "description": "Dealer ID number", - "concerns": "Dealer ID", - }, - "DV": { - "code": "DV", - "type": "Access Denied - Unauthorized Entry Level", - "description": "An access request was denied because the user is not authorized in this area", - "concerns": "Door number", - }, - "DW": { - "code": "DW", - "type": "Access Denied - Interlock", - "description": "An access request was denied because the doors associated Interlock point is open", - "concerns": "Door number", - }, - "DX": { - "code": "DX", - "type": "Request to Exit", - "description": "An access point was opened via a Request to Exit device", - "concerns": "Door number", - }, - "DY": { - "code": "DY", - "type": "Door Locked", - "description": "The door’s lock has been engaged", - "concerns": "Door number", - }, - "DZ": { - "code": "DZ", - "type": "Access Denied - Door Secured", - "description": "An access request was denied because the door has been placed in an Access Closed state", - "concerns": "Door number", - }, - "EA": { - "code": "EA", - "type": "Exit Alarm", - "description": "An exit zone remained violated at the end of the exit delay period", - "concerns": "Zone or point", - }, - "EE": { - "code": "EE", - "type": "Exit Error", - "description": "An exit zone remained violated at the end of the exit delay period", - "concerns": "User number", - }, - "EJ": { - "code": "EJ", - "type": "Expansion Tamper Restore", - "description": "Expansion device tamper restoral", - "concerns": "Expansion device number", - }, - "EM": { - "code": "EM", - "type": "Expansion Device Missing", - "description": "Expansion device missing", - "concerns": "Expansion device number", - }, - "EN": { - "code": "EN", - "type": "Expansion Missing Restore", - "description": "Expansion device communications re-established", - "concerns": "Expansion device number", - }, - "ER": { - "code": "ER", - "type": "Expansion Restoral", - "description": "Expansion device trouble eliminated", - "concerns": "Expander number", - }, - "ES": { - "code": "ES", - "type": "Expansion Device Tamper", - "description": "Expansion device enclosure tamper", - "concerns": "Expansion device number", - }, - "ET": { - "code": "ET", - "type": "Expansion Trouble", - "description": "Expansion device trouble", - "concerns": "Expander number", - }, - "EX": { - "code": "EX", - "type": "External Device Condition", - "description": "A specific reportable condition is detected on an external device", - "concerns": "Device number", - }, - "EZ": { - "code": "EZ", - "type": "Missing Alarm - Exit Error", - "description": "A point remained missing at the end of the exit delay period", - "concerns": "Point number", - }, - "FA": { - "code": "FA", - "type": "Fire Alarm", - "description": "Fire condition detected", - "concerns": "Zone or point", - }, - "FB": { - "code": "FB", - "type": "Fire Bypass", - "description": "Zone has been bypassed", - "concerns": "Zone or point", - }, - "FC": { - "code": "FC", - "type": "Fire Cancel", - "description": "A Fire Alarm has been cancelled by an authorized person", - "concerns": "Zone or point", - }, - "FG": { - "code": "FG", - "type": "Unverified Event – Fire", - "description": "A point assigned to a Cross Point group has gone into alarm but the Cross Point remained normal", - "concerns": "Zone or point", - }, - "FH": { - "code": "FH", - "type": "Fire Alarm Restore", - "description": "Alarm condition eliminated", - "concerns": "Zone or point", - }, - "FI": { - "code": "FI", - "type": "Fire Test Begin", - "description": "The transmitter area's fire test has begun", - "concerns": "Area number", - }, - "FJ": { - "code": "FJ", - "type": "Fire Trouble Restore", - "description": "Trouble condition eliminated", - "concerns": "Zone or point", - }, - "FK": { - "code": "FK", - "type": "Fire Test End", - "description": "The transmitter area's fire test has ended", - "concerns": "Area number", - }, - "FL": { - "code": "FL", - "type": "Fire Alarm Silenced", - "description": "The fire panel’s sounder was silenced by command", - "concerns": "Zone or point", - }, - "FM": { - "code": "FM", - "type": "Fire Alarm - Cross Point", - "description": "Fire Alarm with Cross Point also in alarm verifying the Fire Alarm", - "concerns": "Point number", - }, - "FQ": { - "code": "FQ", - "type": "Fire Supervisory Trouble Restore", - "description": "A fire supervisory zone that was in trouble condition has now restored to normal", - "concerns": "Zone or point", - }, - "FR": { - "code": "FR", - "type": "Fire Restoral", - "description": "Alarm/trouble condition has been eliminated", - "concerns": "Zone or point", - }, - "FS": { - "code": "FS", - "type": "Fire Supervisory", - "description": "Unsafe fire detection system condition", - "concerns": "Zone or point", - }, - "FT": { - "code": "FT", - "type": "Fire Trouble", - "description": "Zone disabled by fault", - "concerns": "Zone or point", - }, - "FU": { - "code": "FU", - "type": "Fire Unbypass", - "description": "Bypass has been removed", - "concerns": "Zone or point", - }, - "FV": { - "code": "FV", - "type": "Fire Supervision Restore", - "description": "A fire supervision zone that was in alarm has restored to normal", - "concerns": "Zone or point", - }, - "FW": { - "code": "FW", - "type": "Fire Supervisory Trouble", - "description": "A fire supervisory zone is now in a trouble condition", - "concerns": "Zone or point", - }, - "FX": { - "code": "FX", - "type": "Fire Test", - "description": "Fire zone activated during test", - "concerns": "Zone or point", - }, - "FY": { - "code": "FY", - "type": "Missing Fire Trouble", - "description": "A fire point is now logically missing", - "concerns": "Zone or point", - }, - "FZ": { - "code": "FZ", - "type": "Missing Fire Supervision", - "description": "A Fire Supervisory point has gone missing", - "concerns": "Zone or point", - }, - "GA": { - "code": "GA", - "type": "Gas Alarm", - "description": "Gas alarm condition detected", - "concerns": "Zone or point", - }, - "GB": { - "code": "GB", - "type": "Gas Bypass", - "description": "Zone has been bypassed", - "concerns": "Zone or point", - }, - "GH": { - "code": "GH", - "type": "Gas Alarm Restore", - "description": "Alarm condition eliminated", - "concerns": "Zone or point", - }, - "GJ": { - "code": "GJ", - "type": "Gas Trouble Restore", - "description": "Trouble condition eliminated", - "concerns": "Zone or point", - }, - "GR": { - "code": "GR", - "type": "Gas Restoral", - "description": "Alarm/trouble condition has been eliminated", - "concerns": "Zone or point", - }, - "GS": { - "code": "GS", - "type": "Gas Supervisory", - "description": "Unsafe gas detection system condition", - "concerns": "Zone or point", - }, - "GT": { - "code": "GT", - "type": "Gas Trouble", - "description": "Zone disabled by fault", - "concerns": "Zone or point", - }, - "GU": { - "code": "GU", - "type": "Gas Unbypass", - "description": "Bypass has been removed", - "concerns": "Zone or point", - }, - "GX": { - "code": "GX", - "type": "Gas Test", - "description": "Zone activated during test", - "concerns": "Zone or point", - }, - "HA": { - "code": "HA", - "type": "Holdup Alarm", - "description": "Silent alarm, user under duress", - "concerns": "Zone or point", - }, - "HB": { - "code": "HB", - "type": "Holdup Bypass", - "description": "Zone has been bypassed", - "concerns": "Zone or point", - }, - "HH": { - "code": "HH", - "type": "Holdup Alarm Restore", - "description": "Alarm condition eliminated", - "concerns": "Zone or point", - }, - "HJ": { - "code": "HJ", - "type": "Holdup Trouble Restore", - "description": "Trouble condition eliminated", - "concerns": "Zone or point", - }, - "HR": { - "code": "HR", - "type": "Holdup Restoral", - "description": "Alarm/trouble condition has been eliminated", - "concerns": "Zone or point", - }, - "HS": { - "code": "HS", - "type": "Holdup Supervisory", - "description": "Unsafe holdup system condition", - "concerns": "Zone or point", - }, - "HT": { - "code": "HT", - "type": "Holdup Trouble", - "description": "Zone disabled by fault", - "concerns": "Zone or point", - }, - "HU": { - "code": "HU", - "type": "Holdup Unbypass", - "description": "Bypass has been removed", - "concerns": "Zone or point", - }, - "IA": { - "code": "IA", - "type": "Equipment Failure Condition", - "description": "A specific, reportable condition is detected on a device", - "concerns": "Point number", - }, - "IR": { - "code": "IR", - "type": "Equipment Fail - Restoral", - "description": "The equipment condition has been restored to normal", - "concerns": "Point number", - }, - "JA": { - "code": "JA", - "type": "User code Tamper", - "description": "Too many unsuccessful attempts have been made to enter a user ID", - "concerns": "Area number", - }, - "JD": { - "code": "JD", - "type": "Date Changed", - "description": "The date was changed in the transmitter/receiver", - "concerns": "User number", - }, - "JH": { - "code": "JH", - "type": "Holiday Changed", - "description": "The transmitter's holiday schedule has been changed", - "concerns": "User number", - }, - "JK": { - "code": "JK", - "type": "Latchkey Alert", - "description": "A designated user passcode has not been entered during a scheduled time window", - "concerns": "User number", - }, - "JL": { - "code": "JL", - "type": "Log Threshold", - "description": "The transmitter's log memory has reached its threshold level", - "concerns": "Unused", - }, - "JO": { - "code": "JO", - "type": "Log Overflow", - "description": "The transmitter's log memory has overflowed", - "concerns": "Unused", - }, - "JP": { - "code": "JP", - "type": "User On Premises", - "description": "A designated user passcode has been used to gain access to the premises.", - "concerns": "User number", - }, - "JR": { - "code": "JR", - "type": "Schedule Executed", - "description": "An automatic scheduled event was executed", - "concerns": "Area number", - }, - "JS": { - "code": "JS", - "type": "Schedule Changed", - "description": "An automatic schedule was changed", - "concerns": "User number", - }, - "JT": { - "code": "JT", - "type": "Time Changed", - "description": "The time was changed in the transmitter/receiver", - "concerns": "User number", - }, - "JV": { - "code": "JV", - "type": "User code Changed", - "description": "A user's code has been changed", - "concerns": "User number", - }, - "JX": { - "code": "JX", - "type": "User code Deleted", - "description": "A user's code has been removed", - "concerns": "User number", - }, - "JY": { - "code": "JY", - "type": "User code Added", - "description": "A user’s code has been added", - "concerns": "User number", - }, - "JZ": { - "code": "JZ", - "type": "User Level Set", - "description": "A user’s authority level has been set", - "concerns": "User number", - }, - "KA": { - "code": "KA", - "type": "Heat Alarm", - "description": "High temperature detected on premise", - "concerns": "Zone or point", - }, - "KB": { - "code": "KB", - "type": "Heat Bypass", - "description": "Zone has been bypassed", - "concerns": "Zone or point", - }, - "KH": { - "code": "KH", - "type": "Heat Alarm Restore", - "description": "Alarm condition eliminated", - "concerns": "Zone or point", - }, - "KJ": { - "code": "KJ", - "type": "Heat Trouble Restore", - "description": "Trouble condition eliminated", - "concerns": "Zone or point", - }, - "KR": { - "code": "KR", - "type": "Heat Restoral", - "description": "Alarm/trouble condition has been eliminated", - "concerns": "Zone or point", - }, - "KS": { - "code": "KS", - "type": "Heat Supervisory", - "description": "Unsafe heat detection system condition", - "concerns": "Zone or point", - }, - "KT": { - "code": "KT", - "type": "Heat Trouble", - "description": "Zone disabled by fault", - "concerns": "Zone or point", - }, - "KU": { - "code": "KU", - "type": "Heat Unbypass", - "description": "Bypass has been removed", - "concerns": "Zone or point", - }, - "LB": { - "code": "LB", - "type": "Local Program", - "description": "Begin local programming", - "concerns": "Unused", - }, - "LD": { - "code": "LD", - "type": "Local Program Denied", - "description": "Access code incorrect", - "concerns": "Unused", - }, - "LE": { - "code": "LE", - "type": "Listen-in Ended", - "description": "The listen-in session has been terminated", - "concerns": "Unused", - }, - "LF": { - "code": "LF", - "type": "Listen-in Begin", - "description": "The listen-in session with the RECEIVER has begun", - "concerns": "Unused", - }, - "LR": { - "code": "LR", - "type": "Phone Line Restoral", - "description": "Phone line restored to service", - "concerns": "Line number", - }, - "LS": { - "code": "LS", - "type": "Local Program Success", - "description": "Local programming successful", - "concerns": "Unused", - }, - "LT": { - "code": "LT", - "type": "Phone Line Trouble", - "description": "Phone line trouble report", - "concerns": "Line number", - }, - "LU": { - "code": "LU", - "type": "Local Program Fail", - "description": "Local programming unsuccessful", - "concerns": "Unused", - }, - "LX": { - "code": "LX", - "type": "Local Programming Ended", - "description": "A local programming session has been terminated", - "concerns": "Unused", - }, - "MA": { - "code": "MA", - "type": "Medical Alarm", - "description": "Emergency assistance request", - "concerns": "Zone or point", - }, - "MB": { - "code": "MB", - "type": "Medical Bypass", - "description": "Zone has been bypassed", - "concerns": "Zone or point", - }, - "MH": { - "code": "MH", - "type": "Medical Alarm Restore", - "description": "Alarm condition eliminated", - "concerns": "Zone or point", - }, - "MI": { - "code": "MI", - "type": "Message", - "description": "A canned message is being sent", - "concerns": "Message number", - }, - "MJ": { - "code": "MJ", - "type": "Medical Trouble Restore", - "description": "Trouble condition eliminated", - "concerns": "Zone or point", - }, - "MR": { - "code": "MR", - "type": "Medical Restoral", - "description": "Alarm/trouble condition has been eliminated", - "concerns": "Zone or point", - }, - "MS": { - "code": "MS", - "type": "Medical Supervisory", - "description": "Unsafe system condition exists", - "concerns": "Zone or point", - }, - "MT": { - "code": "MT", - "type": "Medical Trouble", - "description": "Zone disabled by fault", - "concerns": "Zone or point", - }, - "MU": { - "code": "MU", - "type": "Medical Unbypass", - "description": "Bypass has been removed", - "concerns": "Zone or point", - }, - "NA": { - "code": "NA", - "type": "No Activity", - "description": "There has been no zone activity for a programmed amount of time", - "concerns": "Zone number", - }, - "NC": { - "code": "NC", - "type": "Network Condition", - "description": "A communications network has a specific reportable condition", - "concerns": "Network number", - }, - "NF": { - "code": "NF", - "type": "Forced Perimeter Arm", - "description": "Some zones/points not ready", - "concerns": "Area number", - }, - "NL": { - "code": "NL", - "type": "Perimeter Armed", - "description": "An area has been perimeter armed", - "concerns": "Area number", - }, - "NM": { - "code": "NM", - "type": "Perimeter Armed, User Defined", - "description": "A user defined area has been perimeter armed", - "concerns": "Area number", - }, - "NR": { - "code": "NR", - "type": "Network Restoral", - "description": "A communications network has returned to normal operation", - "concerns": "Network number", - }, - "NS": { - "code": "NS", - "type": "Activity Resumed", - "description": "A zone has detected activity after an alert", - "concerns": "Zone number", - }, - "NT": { - "code": "NT", - "type": "Network Failure", - "description": "A communications network has failed", - "concerns": "Network number", - }, - "OA": { - "code": "OA", - "type": "Automatic Opening", - "description": "System has disarmed automatically", - "concerns": "Area number", - }, - "OC": { - "code": "OC", - "type": "Cancel Report", - "description": "Untyped zone cancel", - "concerns": "User number", - }, - "OG": { - "code": "OG", - "type": "Open Area", - "description": "System has been partially disarmed", - "concerns": "Area number", - }, - "OH": { - "code": "OH", - "type": "Early to Open from Alarm", - "description": "An area in alarm was disarmed before the opening window", - "concerns": "User number", - }, - "OI": { - "code": "OI", - "type": "Fail to Open", - "description": "An area has not been armed at the end of the opening window", - "concerns": "Area number", - }, - "OJ": { - "code": "OJ", - "type": "Late Open", - "description": "An area was disarmed after the opening window", - "concerns": "User number", - }, - "OK": { - "code": "OK", - "type": "Early Open", - "description": "An area was disarmed before the opening window", - "concerns": "User number", - }, - "OL": { - "code": "OL", - "type": "Late to Open from Alarm", - "description": "An area in alarm was disarmed after the opening window", - "concerns": "User number", - }, - "OP": { - "code": "OP", - "type": "Opening Report", - "description": "Account was disarmed", - "concerns": "User number", - }, - "OQ": { - "code": "OQ", - "type": "Remote Opening", - "description": "The system was disarmed from a remote location", - "concerns": "User number", - }, - "OR": { - "code": "OR", - "type": "Disarm From Alarm", - "description": "Account in alarm was reset/disarmed", - "concerns": "User number", - }, - "OS": { - "code": "OS", - "type": "Opening Keyswitch", - "description": "Account has been disarmed by keyswitch", - "concerns": "Zone or point", - }, - "OT": { - "code": "OT", - "type": "Late To Close", - "description": "System was not armed on time", - "concerns": "User number", - }, - "OU": { - "code": "OU", - "type": "Output State – Trouble", - "description": "An output on a peripheral device or NAC is not functioning", - "concerns": "Output number", - }, - "OV": { - "code": "OV", - "type": "Output State – Restore", - "description": "An output on a peripheral device or NAC is back to normal operation", - "concerns": "Output number", - }, - "OZ": { - "code": "OZ", - "type": "Point Opening", - "description": "A point, rather than a full area or account, disarmed", - "concerns": "Zone or point", - }, - "PA": { - "code": "PA", - "type": "Panic Alarm", - "description": "Emergency assistance request, manually activated", - "concerns": "Zone or point", - }, - "PB": { - "code": "PB", - "type": "Panic Bypass", - "description": "Panic zone has been bypassed", - "concerns": "Zone or point", - }, - "PH": { - "code": "PH", - "type": "Panic Alarm Restore", - "description": "Alarm condition eliminated", - "concerns": "Zone or point", - }, - "PJ": { - "code": "PJ", - "type": "Panic Trouble Restore", - "description": "Trouble condition eliminated", - "concerns": "Zone or point", - }, - "PR": { - "code": "PR", - "type": "Panic Restoral", - "description": "Alarm/trouble condition has been eliminated", - "concerns": "Zone or point", - }, - "PS": { - "code": "PS", - "type": "Panic Supervisory", - "description": "Unsafe system condition exists", - "concerns": "Zone or point", - }, - "PT": { - "code": "PT", - "type": "Panic Trouble", - "description": "Zone disabled by fault", - "concerns": "Zone or point", - }, - "PU": { - "code": "PU", - "type": "Panic Unbypass", - "description": "Panic zone bypass has been removed", - "concerns": "Zone or point", - }, - "QA": { - "code": "QA", - "type": "Emergency Alarm", - "description": "Emergency assistance request", - "concerns": "Zone or point", - }, - "QB": { - "code": "QB", - "type": "Emergency Bypass", - "description": "Zone has been bypassed", - "concerns": "Zone or point", - }, - "QH": { - "code": "QH", - "type": "Emergency Alarm Restore", - "description": "Alarm condition has been eliminated", - "concerns": "Zone or point", - }, - "QJ": { - "code": "QJ", - "type": "Emergency Trouble Restore", - "description": "Trouble condition has been eliminated", - "concerns": "Zone or point", - }, - "QR": { - "code": "QR", - "type": "Emergency Restoral", - "description": "Alarm/trouble condition has been eliminated", - "concerns": "Zone or point", - }, - "QS": { - "code": "QS", - "type": "Emergency Supervisory", - "description": "Unsafe system condition exists", - "concerns": "Zone or point", - }, - "QT": { - "code": "QT", - "type": "Emergency Trouble", - "description": "Zone disabled by fault", - "concerns": "Zone or point", - }, - "QU": { - "code": "QU", - "type": "Emergency Unbypass", - "description": "Bypass has been removed", - "concerns": "Zone or point", - }, - "RA": { - "code": "RA", - "type": "Remote Programmer Call Failed", - "description": "Transmitter failed to communicate with the remote programmer", - "concerns": "Unused", - }, - "RB": { - "code": "RB", - "type": "Remote Program Begin", - "description": "Remote programming session initiated", - "concerns": "Unused", - }, - "RC": { - "code": "RC", - "type": "Relay Close", - "description": "A relay has energized", - "concerns": "Relay number", - }, - "RD": { - "code": "RD", - "type": "Remote Program Denied", - "description": "Access passcode incorrect", - "concerns": "Unused", - }, - "RN": { - "code": "RN", - "type": "Remote Reset", - "description": "A TRANSMITTER was reset via a remote programmer", - "concerns": "Unused", - }, - "RO": { - "code": "RO", - "type": "Relay Open", - "description": "A relay has de-energized", - "concerns": "Relay number", - }, - "RP": { - "code": "RP", - "type": "Automatic Test", - "description": "Automatic communication test report", - "concerns": "Unused", - }, - "RR": { - "code": "RR", - "type": "Power Up", - "description": "System lost power, is now restored", - "concerns": "Unused", - }, - "RS": { - "code": "RS", - "type": "Remote Program Success", - "description": "Remote programming successful", - "concerns": "Unused", - }, - "RT": { - "code": "RT", - "type": "Data Lost", - "description": "Dialer data lost, transmission error", - "concerns": "Line number", - }, - "RU": { - "code": "RU", - "type": "Remote Program Fail", - "description": "Remote programming unsuccessful", - "concerns": "Unused", - }, - "RX": { - "code": "RX", - "type": "Manual Test", - "description": "Manual communication test report", - "concerns": "User number", - }, - "RY": { - "code": "RY", - "type": "Test Off Normal", - "description": "Test signal(s) indicates abnormal condition(s) exist", - "concerns": "Zone or point", - }, - "SA": { - "code": "SA", - "type": "Sprinkler Alarm", - "description": "Sprinkler flow condition exists", - "concerns": "Zone or point", - }, - "SB": { - "code": "SB", - "type": "Sprinkler Bypass", - "description": "Sprinkler zone has been bypassed", - "concerns": "Zone or point", - }, - "SC": { - "code": "SC", - "type": "Change of State", - "description": "An expansion/peripheral device is reporting a new condition or state change", - "concerns": "Condition number", - }, - "SH": { - "code": "SH", - "type": "Sprinkler Alarm Restore", - "description": "Alarm condition eliminated", - "concerns": "Zone or point", - }, - "SJ": { - "code": "SJ", - "type": "Sprinkler Trouble Restore", - "description": "Trouble condition eliminated", - "concerns": "Zone or point", - }, - "SR": { - "code": "SR", - "type": "Sprinkler Restoral", - "description": "Alarm/trouble condition has been eliminated", - "concerns": "Zone or point", - }, - "SS": { - "code": "SS", - "type": "Sprinkler Supervisory", - "description": "Unsafe sprinkler system condition", - "concerns": "Zone or point", - }, - "ST": { - "code": "ST", - "type": "Sprinkler Trouble", - "description": "Zone disabled by fault", - "concerns": "Zone or point", - }, - "SU": { - "code": "SU", - "type": "Sprinkler Unbypass", - "description": "Sprinkler zone bypass has been removed", - "concerns": "Zone or point", - }, - "TA": { - "code": "TA", - "type": "Tamper Alarm", - "description": "Alarm equipment enclosure opened", - "concerns": "Zone or point", - }, - "TB": { - "code": "TB", - "type": "Tamper Bypass", - "description": "Tamper detection has been bypassed", - "concerns": "Zone or point", - }, - "TC": { - "code": "TC", - "type": "All Points Tested", - "description": "All point tested", - "concerns": "Unused", - }, - "TE": { - "code": "TE", - "type": "Test End", - "description": "Communicator restored to operation", - "concerns": "Unused", - }, - "TH": { - "code": "TH", - "type": "Tamper Alarm Restore", - "description": "An Expansion Device’s tamper switch restores to normal from an Alarm state", - "concerns": "Unused", - }, - "TJ": { - "code": "TJ", - "type": "Tamper Trouble Restore", - "description": "An Expansion Device’s tamper switch restores to normal from a Trouble state", - "concerns": "Unused", - }, - "TP": { - "code": "TP", - "type": "Walk Test Point", - "description": "This point was tested during a Walk Test", - "concerns": "Point number", - }, - "TR": { - "code": "TR", - "type": "Tamper Restoral", - "description": "Alarm equipment enclosure has been closed", - "concerns": "Zone or point", - }, - "TS": { - "code": "TS", - "type": "Test Start", - "description": "Communicator taken out of operation", - "concerns": "Unused", - }, - "TT": { - "code": "TT", - "type": "Tamper Trouble", - "description": "Equipment enclosure opened in disarmed state", - "concerns": "Zone or point", - }, - "TU": { - "code": "TU", - "type": "Tamper Unbypass", - "description": "Tamper detection bypass has been removed", - "concerns": "Zone or point", - }, - "TW": { - "code": "TW", - "type": "Area Watch Start", - "description": "Area watch feature has been activated", - "concerns": "Unused", - }, - "TX": { - "code": "TX", - "type": "Test Report", - "description": "An unspecified (manual or automatic) communicator test", - "concerns": "Unused", - }, - "TZ": { - "code": "TZ", - "type": "Area Watch End", - "description": "Area watch feature has been deactivated", - "concerns": "Unused", - }, - "UA": { - "code": "UA", - "type": "Untyped Zone Alarm", - "description": "Alarm condition from zone of unknown type", - "concerns": "Zone or point", - }, - "UB": { - "code": "UB", - "type": "Untyped Zone Bypass", - "description": "Zone of unknown type has been bypassed", - "concerns": "Zone or point", - }, - "UG": { - "code": "UG", - "type": "Unverified Event – Untyped", - "description": "A point assigned to a Cross Point group has gone into alarm but the Cross Point remained normal", - "concerns": "Zone or point", - }, - "UH": { - "code": "UH", - "type": "Untyped Alarm Restore", - "description": "Alarm condition eliminated", - "concerns": "Zone or point", - }, - "UJ": { - "code": "UJ", - "type": "Untyped Trouble Restore", - "description": "Trouble condition eliminated", - "concerns": "Zone or point", - }, - "UR": { - "code": "UR", - "type": "Untyped Zone Restoral", - "description": "Alarm/trouble condition eliminated from zone of unknown type", - "concerns": "Zone or point", - }, - "US": { - "code": "US", - "type": "Untyped Zone Supervisory", - "description": "Unsafe condition from zone of unknown type", - "concerns": "Zone or point", - }, - "UT": { - "code": "UT", - "type": "Untyped Zone Trouble", - "description": "Trouble condition from zone of unknown type", - "concerns": "Zone or point", - }, - "UU": { - "code": "UU", - "type": "Untyped Zone Unbypass", - "description": "Bypass on zone of unknown type has been removed", - "concerns": "Zone or point", - }, - "UX": { - "code": "UX", - "type": "Undefined", - "description": "An undefined alarm condition has occurred", - "concerns": "Unused", - }, - "UY": { - "code": "UY", - "type": "Untyped Missing Trouble", - "description": "A point or device which was not armed is now logically missing", - "concerns": "Zone or point", - }, - "UZ": { - "code": "UZ", - "type": "Untyped Missing Alarm", - "description": "A point or device which was armed is now logically missing", - "concerns": "Zone or point", - }, - "VI": { - "code": "VI", - "type": "Printer Paper In", - "description": "TRANSMITTER or RECEIVER paper in", - "concerns": "Printer number", - }, - "VO": { - "code": "VO", - "type": "Printer Paper Out", - "description": "TRANSMITTER or RECEIVER paper out", - "concerns": "Printer number", - }, - "VR": { - "code": "VR", - "type": "Printer Restore", - "description": "TRANSMITTER or RECEIVER trouble restored", - "concerns": "Printer number", - }, - "VT": { - "code": "VT", - "type": "Printer Trouble", - "description": "TRANSMITTER or RECEIVER trouble", - "concerns": "Printer number", - }, - "VX": { - "code": "VX", - "type": "Printer Test", - "description": "TRANSMITTER or RECEIVER test", - "concerns": "Printer number", - }, - "VY": { - "code": "VY", - "type": "Printer Online", - "description": "RECEIVER’S printer is now online", - "concerns": "Unused", - }, - "VZ": { - "code": "VZ", - "type": "Printer Offline", - "description": "RECEIVER’S printer is now offline", - "concerns": "Unused", - }, - "WA": { - "code": "WA", - "type": "Water Alarm", - "description": "Water detected at protected premises", - "concerns": "Zone or point", - }, - "WB": { - "code": "WB", - "type": "Water Bypass", - "description": "Water detection has been bypassed", - "concerns": "Zone or point", - }, - "WH": { - "code": "WH", - "type": "Water Alarm Restore", - "description": "Water alarm condition eliminated", - "concerns": "Zone or point", - }, - "WJ": { - "code": "WJ", - "type": "Water Trouble Restore", - "description": "Water trouble condition eliminated", - "concerns": "Zone or point", - }, - "WR": { - "code": "WR", - "type": "Water Restoral", - "description": "Water alarm/trouble condition has been eliminated", - "concerns": "Zone or point", - }, - "WS": { - "code": "WS", - "type": "Water Supervisory", - "description": "Water unsafe water detection system condition", - "concerns": "Zone or point", - }, - "WT": { - "code": "WT", - "type": "Water Trouble", - "description": "Water zone disabled by fault", - "concerns": "Zone or point", - }, - "WU": { - "code": "WU", - "type": "Water Unbypass", - "description": "Water detection bypass has been removed", - "concerns": "Zone or point", - }, - "XA": { - "code": "XA", - "type": "Extra Account Report", - "description": "CS RECEIVER has received an event from a non-existent account", - "concerns": "Unused", - }, - "XE": { - "code": "XE", - "type": "Extra Point", - "description": "Panel has sensed an extra point not specified for this site", - "concerns": "Point number", - }, - "XF": { - "code": "XF", - "type": "Extra RF Point", - "description": "Panel has sensed an extra RF point not specified for this site", - "concerns": "Point number", - }, - "XH": { - "code": "XH", - "type": "RF Interference Restoral", - "description": "A radio device is no longer detecting RF Interference", - "concerns": "Receiver number", - }, - "XI": { - "code": "XI", - "type": "Sensor Reset", - "description": "A user has reset a sensor", - "concerns": "Zone or point", - }, - "XJ": { - "code": "XJ", - "type": "RF Receiver Tamper Restoral", - "description": "A Tamper condition at a premises RF Receiver has been restored", - "concerns": "Receiver number", - }, - "XL": { - "code": "XL", - "type": "Low Received Signal Strength", - "description": "The RF signal strength of a reported event is below minimum level", - "concerns": "Receiver number", - }, - "XM": { - "code": "XM", - "type": "Missing Alarm - Cross Point", - "description": "Missing Alarm verified by Cross Point in Alarm (or missing)", - "concerns": "Zone or point", - }, - "XQ": { - "code": "XQ", - "type": "RF Interference", - "description": "A radio device is detecting RF Interference", - "concerns": "Receiver number", - }, - "XR": { - "code": "XR", - "type": "Transmitter Battery Restoral", - "description": "Low battery has been corrected", - "concerns": "Zone or point", - }, - "XS": { - "code": "XS", - "type": "RF Receiver Tamper", - "description": "A Tamper condition at a premises receiver is detected", - "concerns": "Receiver number", - }, - "XT": { - "code": "XT", - "type": "Transmitter Battery Trouble", - "description": "Low battery in wireless transmitter", - "concerns": "Zone or point", - }, - "XW": { - "code": "XW", - "type": "Forced Point", - "description": "A point was forced out of the system at arm time", - "concerns": "Zone or point", - }, - "XX": { - "code": "XX", - "type": "Fail to Test", - "description": "A specific test from a panel was not received", - "concerns": "Unused", - }, - "YA": { - "code": "YA", - "type": "Bell Fault", - "description": "A trouble condition has been detected on a Local Bell, Siren, or Annunciator", - "concerns": "Unused", - }, - "YB": { - "code": "YB", - "type": "Busy Seconds", - "description": "Percent of time receiver's line card is on-line", - "concerns": "Line card number", - }, - "YC": { - "code": "YC", - "type": "Communications Fail", - "description": "RECEIVER and TRANSMITTER", - "concerns": "Unused", - }, - "YD": { - "code": "YD", - "type": "Receiver Line Card Trouble", - "description": "A line card identified by the passed address is in trouble", - "concerns": "Line card number", - }, - "YE": { - "code": "YE", - "type": "Receiver Line Card Restored", - "description": "A line card identified by the passed address is restored", - "concerns": "Line card number", - }, - "YF": { - "code": "YF", - "type": "Parameter Checksum Fail", - "description": "System data corrupted", - "concerns": "Unused", - }, - "YG": { - "code": "YG", - "type": "Parameter Changed", - "description": "A TRANSMITTER’S parameters have been changed", - "concerns": "Unused", - }, - "YH": { - "code": "YH", - "type": "Bell Restored", - "description": "A trouble condition has been restored on a Local Bell, Siren, or Annunciator", - "concerns": "Unused", - }, - "YI": { - "code": "YI", - "type": "Overcurrent Trouble", - "description": "An Expansion Device has detected an overcurrent condition", - "concerns": "Unused", - }, - "YJ": { - "code": "YJ", - "type": "Overcurrent Restore", - "description": "An Expansion Device has restored from an overcurrent condition", - "concerns": "Unused", - }, - "YK": { - "code": "YK", - "type": "Communications Restoral", - "description": "TRANSMITTER has resumed communication with a RECEIVER", - "concerns": "Unused", - }, - "YM": { - "code": "YM", - "type": "System Battery Missing", - "description": "TRANSMITTER/RECEIVER battery is missing", - "concerns": "Unused", - }, - "YN": { - "code": "YN", - "type": "Invalid Report", - "description": "TRANSMITTER has sent a packet with invalid data", - "concerns": "Unused", - }, - "YO": { - "code": "YO", - "type": "Unknown Message", - "description": "An unknown message was received from automation or the printer", - "concerns": "Unused", - }, - "YP": { - "code": "YP", - "type": "Power Supply Trouble", - "description": "TRANSMITTER/RECEIVER has a problem with the power supply", - "concerns": "Unused", - }, - "YQ": { - "code": "YQ", - "type": "Power Supply Restored", - "description": "TRANSMITTER’S/RECEIVER’S power supply has been restored", - "concerns": "Unused", - }, - "YR": { - "code": "YR", - "type": "System Battery Restoral", - "description": "Low battery has been corrected", - "concerns": "Unused", - }, - "YS": { - "code": "YS", - "type": "Communications Trouble", - "description": "RECEIVER and TRANSMITTER", - "concerns": "Unused", - }, - "YT": { - "code": "YT", - "type": "System Battery Trouble", - "description": "Low battery in control/communicator", - "concerns": "Unused", - }, - "YU": { - "code": "YU", - "type": "Diagnostic Error", - "description": "An expansion/peripheral device is reporting a diagnostic error", - "concerns": "Condition number", - }, - "YW": { - "code": "YW", - "type": "Watchdog Reset", - "description": "The TRANSMITTER created an internal reset", - "concerns": "Unused", - }, - "YX": { - "code": "YX", - "type": "Service Required", - "description": "A TRANSMITTER/RECEIVER needs service", - "concerns": "Unused", - }, - "YY": { - "code": "YY", - "type": "Status Report", - "description": "This is a header for an account status report transmission", - "concerns": "Unused", - }, - "YZ": { - "code": "YZ", - "type": "Service Completed", - "description": "Required TRANSMITTER / RECEIVER service completed", - "concerns": "Mfr defined", - }, - "ZA": { - "code": "ZA", - "type": "Freeze Alarm", - "description": "Low temperature detected at premises", - "concerns": "Zone or point", - }, - "ZB": { - "code": "ZB", - "type": "Freeze Bypass", - "description": "Low temperature detection has been bypassed", - "concerns": "Zone or point", - }, - "ZH": { - "code": "ZH", - "type": "Freeze Alarm Restore", - "description": "Alarm condition eliminated", - "concerns": "Zone or point", - }, - "ZJ": { - "code": "ZJ", - "type": "Freeze Trouble Restore", - "description": "Trouble condition eliminated", - "concerns": "Zone or point", - }, - "ZR": { - "code": "ZR", - "type": "Freeze Restoral", - "description": "Alarm/trouble condition has been eliminated", - "concerns": "Zone or point", - }, - "ZS": { - "code": "ZS", - "type": "Freeze Supervisory", - "description": "Unsafe freeze detection system condition", - "concerns": "Zone or point", - }, - "ZT": { - "code": "ZT", - "type": "Freeze Trouble", - "description": "Zone disabled by fault", - "concerns": "Zone or point", - }, - "ZU": { - "code": "ZU", - "type": "Freeze Unbypass", - "description": "Low temperature detection bypass removed", - "concerns": "Zone or point", - }, - } diff --git a/custom_components/sia/strings.json b/custom_components/sia/strings.json new file mode 100644 index 0000000..1eed82a --- /dev/null +++ b/custom_components/sia/strings.json @@ -0,0 +1,31 @@ +{ + "title": "SIA Alarm Systems", + "config": { + "step": { + "user": { + "data": { + "name": "Name", + "port": "Port", + "account": "Account", + "encryption_key": "Encryption Key", + "ping_interval": "Ping Interval (min)", + "zones": "Number of zones for the account", + "additional_account": "Add more accounts?" + }, + "title": "Create a connection for SIA DC-09 based alarm systems." + } + }, + "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_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.", + "invalid_zones": "There needs to be at least 1 zone.", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "This SIA Port is already used, please select another or recreate the existing with an extra account." + } + } +} diff --git a/custom_components/sia/translations/en.json b/custom_components/sia/translations/en.json new file mode 100644 index 0000000..9cc5202 --- /dev/null +++ b/custom_components/sia/translations/en.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "This SIA Port is already used, please select another or recreate the existing with an extra account." + }, + "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_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.", + "invalid_zones": "There needs to be at least 1 zone.", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "name": "Name", + "port": "Port", + "account": "Account", + "encryption_key": "Encryption Key", + "ping_interval": "Ping Interval (min)", + "zones": "Number of zones for the account", + "additional_account": "Add more accounts?" + }, + "title": "Create a connection for SIA DC-09 based alarm systems." + } + } + }, + "title": "SIA Alarm Systems" +} \ No newline at end of file