Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
84f8cc1
initial commit of SIA integration
eavanvalkenburg May 27, 2020
219f0e7
translations
eavanvalkenburg Jun 10, 2020
92fbe28
moved reactions to file, typed everything
eavanvalkenburg Jun 10, 2020
eceff78
fixed no-else-return 3 times
eavanvalkenburg Jun 11, 2020
78b10c4
refactored config and fix coverage of test
eavanvalkenburg Jun 17, 2020
88cc15e
fix requirements_test
eavanvalkenburg Jun 17, 2020
e29cce6
elimated another platform
eavanvalkenburg Aug 20, 2020
e6d1cc4
forgot some mentions of sensor
eavanvalkenburg Aug 20, 2020
acd8059
updated config flow steps, fixed restore and small edits
eavanvalkenburg Aug 25, 2020
6c936df
fixed pylint
eavanvalkenburg Aug 25, 2020
346ee55
updated config_flow with better schema, small fixes from review
eavanvalkenburg Aug 25, 2020
f0ca2aa
final comment and small legibility enhancements
eavanvalkenburg Aug 28, 2020
12bfc99
small fix for pylint
eavanvalkenburg Aug 28, 2020
11819d5
fixed init
eavanvalkenburg Aug 28, 2020
0dd74c2
fixes for botched rebase
eavanvalkenburg Aug 28, 2020
09b7fae
fixed port string
eavanvalkenburg Sep 3, 2020
4f87cb1
updated common strings
eavanvalkenburg Oct 6, 2020
3381d77
rebuild component with eventbus
eavanvalkenburg Feb 4, 2021
e25a97c
fixed pylint and tests
eavanvalkenburg Feb 8, 2021
e9a18df
updates based on review by @bdraco
eavanvalkenburg Feb 10, 2021
7c90ba5
updates based on new version of package and reviews
eavanvalkenburg Apr 9, 2021
b677307
small updates with latest package
eavanvalkenburg Apr 12, 2021
f0ec788
added raise from
eavanvalkenburg Apr 12, 2021
66d064d
deleted async_setup from test
eavanvalkenburg Apr 13, 2021
2a0ab12
fixed tests
eavanvalkenburg Apr 13, 2021
cc01c83
removed unused code from addititional account step
eavanvalkenburg Apr 13, 2021
da28c10
fixed typo in strings
eavanvalkenburg Apr 14, 2021
e8e6532
clarification and update to update_data func
eavanvalkenburg Apr 19, 2021
cf20bac
added iot_class to manifest
eavanvalkenburg Apr 19, 2021
81fd57e
fixed entity and unique id setup
eavanvalkenburg Apr 22, 2021
3914ce4
small fix in tests
eavanvalkenburg Apr 22, 2021
d007d12
improved unique_id semantics and load/unload functions
eavanvalkenburg Apr 30, 2021
fd71c49
added typing in order to fix mypy
eavanvalkenburg Apr 30, 2021
4541dc2
further fixes for typing
eavanvalkenburg Apr 30, 2021
40c77a8
final fixes for mypy
eavanvalkenburg Apr 30, 2021
409fc1a
adding None return types
eavanvalkenburg May 3, 2021
6673927
fix hub DR identifier
eavanvalkenburg May 3, 2021
def09c8
rebased, added DeviceInfo
eavanvalkenburg May 3, 2021
2838699
rewrite to clean up and make it easier to read
eavanvalkenburg May 3, 2021
e16459d
replaced functions with format for id and name
eavanvalkenburg May 3, 2021
9b6ed50
renamed tracker remover small fix in state.setter
eavanvalkenburg May 4, 2021
d2638ce
improved readibility of state.setter
eavanvalkenburg May 4, 2021
42d1fd0
no more state.setter and small updates
eavanvalkenburg May 5, 2021
8bc7c29
mypy fix
eavanvalkenburg May 5, 2021
99b3e5c
fixed and improved config flow
eavanvalkenburg May 5, 2021
0800bac
added fixtures to test and other cleaner test code
eavanvalkenburg May 6, 2021
74aa738
removed timeband from config, will reintro in a options flow
eavanvalkenburg May 7, 2021
ac66f04
removed timeband from tests
eavanvalkenburg May 7, 2021
10e7b26
added options flow for zones and timestamps
eavanvalkenburg May 10, 2021
818be2e
removed type ignore
eavanvalkenburg May 10, 2021
f3e4c2c
replaced mapping with collections.abc
eavanvalkenburg May 10, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,11 @@ omit =
homeassistant/components/skybeacon/sensor.py
homeassistant/components/skybell/*
homeassistant/components/slack/notify.py
homeassistant/components/sia/__init__.py
homeassistant/components/sia/alarm_control_panel.py
homeassistant/components/sia/const.py
homeassistant/components/sia/hub.py
homeassistant/components/sia/utils.py
homeassistant/components/sinch/*
homeassistant/components/slide/*
homeassistant/components/sma/__init__.py
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ repos:
hooks:
- id: codespell
args:
- --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort
- --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba
- --skip="./.*,*.csv,*.json"
- --quiet-level=2
exclude_types: [csv, json]
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ homeassistant/components/shell_command/* @home-assistant/core
homeassistant/components/shelly/* @balloob @bieniu @thecode @chemelli74
homeassistant/components/shiftr/* @fabaff
homeassistant/components/shodan/* @fabaff
homeassistant/components/sia/* @eavanvalkenburg
homeassistant/components/sighthound/* @robmarkcole
homeassistant/components/signal_messenger/* @bbernhard
homeassistant/components/simplisafe/* @bachya
Expand Down
34 changes: 34 additions & 0 deletions homeassistant/components/sia/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""The sia integration."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady

from .const import DOMAIN, PLATFORMS
from .hub import SIAHub


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up sia from a config entry."""
hub: SIAHub = SIAHub(hass, entry)
await hub.async_setup_hub()

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = hub
try:
await hub.sia_client.start(reuse_port=True)
except OSError as exc:
raise ConfigEntryNotReady(
f"SIA Server at port {entry.data[CONF_PORT]} could not start."
) from exc
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hub: SIAHub = hass.data[DOMAIN].pop(entry.entry_id)
await hub.async_shutdown()
return unload_ok
253 changes: 253 additions & 0 deletions homeassistant/components/sia/alarm_control_panel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
"""Module for SIA Alarm Control Panels."""
from __future__ import annotations

import logging
from typing import Any, Callable

from pysiaalarm import SIAEvent

from homeassistant.components.alarm_control_panel import (
ENTITY_ID_FORMAT as ALARM_ENTITY_ID_FORMAT,
AlarmControlPanelEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_PORT,
CONF_ZONE,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
STATE_UNAVAILABLE,
)
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import StateType

from .const import (
CONF_ACCOUNT,
CONF_ACCOUNTS,
CONF_PING_INTERVAL,
CONF_ZONES,
DOMAIN,
SIA_ENTITY_ID_FORMAT,
SIA_EVENT,
SIA_NAME_FORMAT,
SIA_UNIQUE_ID_FORMAT_ALARM,
)
from .utils import get_attr_from_sia_event, get_unavailability_interval

_LOGGER = logging.getLogger(__name__)

DEVICE_CLASS_ALARM = "alarm"
PREVIOUS_STATE = "previous_state"

CODE_CONSEQUENCES: dict[str, StateType] = {
"PA": STATE_ALARM_TRIGGERED,
"JA": STATE_ALARM_TRIGGERED,
"TA": STATE_ALARM_TRIGGERED,
"BA": STATE_ALARM_TRIGGERED,
"CA": STATE_ALARM_ARMED_AWAY,
"CB": STATE_ALARM_ARMED_AWAY,
"CG": STATE_ALARM_ARMED_AWAY,
"CL": STATE_ALARM_ARMED_AWAY,
"CP": STATE_ALARM_ARMED_AWAY,
"CQ": STATE_ALARM_ARMED_AWAY,
"CS": STATE_ALARM_ARMED_AWAY,
"CF": STATE_ALARM_ARMED_CUSTOM_BYPASS,
"OA": STATE_ALARM_DISARMED,
"OB": STATE_ALARM_DISARMED,
"OG": STATE_ALARM_DISARMED,
"OP": STATE_ALARM_DISARMED,
"OQ": STATE_ALARM_DISARMED,
"OR": STATE_ALARM_DISARMED,
"OS": STATE_ALARM_DISARMED,
"NC": STATE_ALARM_ARMED_NIGHT,
"NL": STATE_ALARM_ARMED_NIGHT,
"BR": PREVIOUS_STATE,
"NP": PREVIOUS_STATE,
"NO": PREVIOUS_STATE,
}


async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: Callable[..., None],
Comment thread
eavanvalkenburg marked this conversation as resolved.
) -> bool:
Comment thread
eavanvalkenburg marked this conversation as resolved.
"""Set up SIA alarm_control_panel(s) from a config entry."""
async_add_entities(
[
Comment thread
eavanvalkenburg marked this conversation as resolved.
SIAAlarmControlPanel(entry, account_data, zone)
for account_data in entry.data[CONF_ACCOUNTS]
for zone in range(
1,
entry.options[CONF_ACCOUNTS][account_data[CONF_ACCOUNT]][CONF_ZONES]
+ 1,
)
]
)
return True
Comment thread
eavanvalkenburg marked this conversation as resolved.


class SIAAlarmControlPanel(AlarmControlPanelEntity, RestoreEntity):
"""Class for SIA Alarm Control Panels."""

def __init__(
self,
entry: ConfigEntry,
account_data: dict[str, Any],
zone: int,
):
"""Create SIAAlarmControlPanel object."""
self._entry: ConfigEntry = entry
self._account_data: dict[str, Any] = account_data
self._zone: int = zone

self._port: int = self._entry.data[CONF_PORT]
self._account: str = self._account_data[CONF_ACCOUNT]
self._ping_interval: int = self._account_data[CONF_PING_INTERVAL]

self.entity_id: str = ALARM_ENTITY_ID_FORMAT.format(
Comment thread
eavanvalkenburg marked this conversation as resolved.
SIA_ENTITY_ID_FORMAT.format(
self._port, self._account, self._zone, DEVICE_CLASS_ALARM
)
)

self._attr: dict[str, Any] = {
CONF_PORT: self._port,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use CONF_* constants for state attribute keys. Use ATTR_* constants instead.

CONF_ACCOUNT: self._account,
CONF_ZONE: self._zone,
CONF_PING_INTERVAL: f"{self._ping_interval} minute(s)",
Comment thread
eavanvalkenburg marked this conversation as resolved.
}

self._available: bool = True
self._state: StateType = None
self._old_state: StateType = None
self._cancel_availability_cb: CALLBACK_TYPE | None = None

async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass.

Overridden from Entity.

1. start the event listener and add the callback to on_remove
2. get previous state from storage
3. if previous state: restore
4. if previous state is unavailable: set _available to False and return
5. if available: create availability cb
"""
self.async_on_remove(
self.hass.bus.async_listen(
Comment thread
eavanvalkenburg marked this conversation as resolved.
event_type=SIA_EVENT.format(self._port, self._account),
listener=self.async_handle_event,
)
)
last_state = await self.async_get_last_state()
if last_state is not None:
self._state = last_state.state
if self.state == STATE_UNAVAILABLE:
self._available = False
return
self._cancel_availability_cb = self.async_create_availability_cb()

async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass.

Overridden from Entity.
"""
if self._cancel_availability_cb:
self._cancel_availability_cb()

async def async_handle_event(self, event: Event) -> None:
"""Listen to events for this port and account and update state and attributes.

If the port and account combo receives any message it means it is online and can therefore be set to available.
"""
sia_event: SIAEvent = SIAEvent.from_dict( # pylint: disable=no-member
event.data
)
_LOGGER.debug("Received event: %s", sia_event)
if int(sia_event.ri) == self._zone:
self._attr.update(get_attr_from_sia_event(sia_event))
new_state = CODE_CONSEQUENCES.get(sia_event.code, None)
if new_state is not None:
if new_state == PREVIOUS_STATE:
new_state = self._old_state
self._state, self._old_state = new_state, self._state
self._available = True
self.async_write_ha_state()
self.async_reset_availability_cb()

@callback
def async_reset_availability_cb(self) -> None:
"""Reset availability cb by cancelling the current and creating a new one."""
if self._cancel_availability_cb:
self._cancel_availability_cb()
self._cancel_availability_cb = self.async_create_availability_cb()

@callback
def async_create_availability_cb(self) -> CALLBACK_TYPE:
"""Create a availability cb and return the callback."""
return async_call_later(
self.hass,
get_unavailability_interval(self._ping_interval),
self.async_set_unavailable,
)

@callback
def async_set_unavailable(self, _) -> None:
"""Set unavailable."""
self._available = False
self.async_write_ha_state()

@property
def state(self) -> StateType:
"""Get state."""
return self._state

@property
def name(self) -> str:
"""Get Name."""
return SIA_NAME_FORMAT.format(
self._port, self._account, self._zone, DEVICE_CLASS_ALARM
)

@property
def unique_id(self) -> str:
"""Get unique_id."""
return SIA_UNIQUE_ID_FORMAT_ALARM.format(
self._entry.entry_id, self._account, self._zone
)

@property
def available(self) -> bool:
"""Get availability."""
return self._available

@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return device attributes."""
return self._attr

@property
def should_poll(self) -> bool:
"""Return False if entity pushes its state to HA."""
return False

@property
def supported_features(self) -> int:
"""Flag supported features."""
return 0

@property
def device_info(self) -> DeviceInfo:
"""Return the device_info."""
return {
"identifiers": {(DOMAIN, self.unique_id)},
"name": self.name,
"via_device": (DOMAIN, f"{self._port}_{self._account}"),
}
Loading