core/homeassistant/components/elkm1/alarm_control_panel.py

234 lines
7.9 KiB
Python
Raw Normal View History

"""Each ElkM1 area will be created as a separate alarm_control_panel."""
from elkm1_lib.const import AlarmState, ArmedStatus, ArmLevel, ArmUpState
import voluptuous as vol
from homeassistant.components.alarm_control_panel import (
FORMAT_NUMBER,
AlarmControlPanel,
)
from homeassistant.components.alarm_control_panel.const import (
SUPPORT_ALARM_ARM_AWAY,
SUPPORT_ALARM_ARM_HOME,
SUPPORT_ALARM_ARM_NIGHT,
)
from homeassistant.const import (
2019-07-31 19:25:30 +00:00
ATTR_CODE,
ATTR_ENTITY_ID,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_ARMING,
STATE_ALARM_DISARMED,
STATE_ALARM_PENDING,
STATE_ALARM_TRIGGERED,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
2019-07-31 19:25:30 +00:00
async_dispatcher_connect,
async_dispatcher_send,
)
from . import (
DOMAIN,
SERVICE_ALARM_ARM_HOME_INSTANT,
SERVICE_ALARM_ARM_NIGHT_INSTANT,
SERVICE_ALARM_ARM_VACATION,
SERVICE_ALARM_DISPLAY_MESSAGE,
ElkEntity,
create_elk_entities,
)
2019-07-31 19:25:30 +00:00
SIGNAL_ARM_ENTITY = "elkm1_arm"
SIGNAL_DISPLAY_MESSAGE = "elkm1_display_message"
2019-07-31 19:25:30 +00:00
ELK_ALARM_SERVICE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID, default=[]): cv.entity_ids,
vol.Required(ATTR_CODE): vol.All(vol.Coerce(int), vol.Range(0, 999999)),
}
)
DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids,
vol.Optional("clear", default=2): vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
vol.Optional("beep", default=False): cv.boolean,
vol.Optional("timeout", default=0): vol.All(
vol.Coerce(int), vol.Range(min=0, max=65535)
),
vol.Optional("line1", default=""): cv.string,
vol.Optional("line2", default=""): cv.string,
}
)
2019-07-31 19:25:30 +00:00
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the ElkM1 alarm platform."""
if discovery_info is None:
return
elk_datas = hass.data[DOMAIN]
Support multiple Elk instances (#23839) * Support multiple Elk instances * Allow more than one Elk M1 alarm system to be integrated into a single hass instance. * Introduces new "devices" schema at the top level, each of which has the prior configuration schema. * Requires new version of elkm1, 0.7.14, that gwww and I just updated (thanks Glen!) QUESTION: Should the "devices" section be optional to avoid breaking old configuration files? I chose not to do that for simplicity and because I was following the doorbird code which requires the "devices" section for all configurations even with only one device. * Fixed a bunch of hound-raised issues Fixed issues raised by hound -- there was clearly a tool I was supposed to run to get those warnings before submitting the PR. Sorry! Updated REQUIREMENTS. * Fixed whitespace and line-length mistakes Also fixed unused prefix local variable lint warning. * Fixed missing blank line * Fixed more lint warnings. Not sure if I missed these on the first pass or if the linter stopped after a certain number of warnings or something else. Switched logging to use %d and %s instead of string concatenation (per lint request and because I imagine it migth be better performing in some (oldish, I presume) implementations of python. * Fixed typo in last commit. * Eliminate devices subsection in config schema This eliminates the breaking change for configurations wanting a singleton elk m1 instance (the majority of users, no doubt). I did not do it like this before because I was following the lead of the doorbird component which introduced a devices: section when moving to support multiple doorbells. But Rohan Kapoor kindly pointed me at the zoneminder component which sets the other (IMO) preferable precedent. Will update the docs change shortly. * Call async_add_entities once for all the elk controllers. Just move async_add_entities() outside of the loops across the elk m1 controllers, so it's called once for each platform. * Call async_add_entities only once per platform. Move it to after the loop, so it's called only once per platform even when there are multiple elk m1 controllers. * Various improvements to be more idiomatic python + bug fixes Thanks to Martin Hjelmare for the careful review and suggestions. (All mistaken improvements and new bugs are my own.) * Removed semicolon that lint caught. * Idiomatic python improvements Use dict.values() (instead of making it easier to add local looping variable on the keys by using _, bar = ...items()) Use [] when the key is known to exist. * Support multiple Elk instances * Allow more than one Elk M1 alarm system to be integrated into a single hass instance. * Introduces new "devices" schema at the top level, each of which has the prior configuration schema. * Requires new version of elkm1, 0.7.14, that gwww and I just updated (thanks Glen!) QUESTION: Should the "devices" section be optional to avoid breaking old configuration files? I chose not to do that for simplicity and because I was following the doorbird code which requires the "devices" section for all configurations even with only one device. * Fixed a bunch of hound-raised issues Fixed issues raised by hound -- there was clearly a tool I was supposed to run to get those warnings before submitting the PR. Sorry! Updated REQUIREMENTS. * Fixed whitespace and line-length mistakes Also fixed unused prefix local variable lint warning. * Fixed missing blank line * Fixed more lint warnings. Not sure if I missed these on the first pass or if the linter stopped after a certain number of warnings or something else. Switched logging to use %d and %s instead of string concatenation (per lint request and because I imagine it migth be better performing in some (oldish, I presume) implementations of python. * Fixed typo in last commit. * Eliminate devices subsection in config schema This eliminates the breaking change for configurations wanting a singleton elk m1 instance (the majority of users, no doubt). I did not do it like this before because I was following the lead of the doorbird component which introduced a devices: section when moving to support multiple doorbells. But Rohan Kapoor kindly pointed me at the zoneminder component which sets the other (IMO) preferable precedent. Will update the docs change shortly. * Call async_add_entities once for all the elk controllers. Just move async_add_entities() outside of the loops across the elk m1 controllers, so it's called once for each platform. * Call async_add_entities only once per platform. Move it to after the loop, so it's called only once per platform even when there are multiple elk m1 controllers. * Various improvements to be more idiomatic python + bug fixes Thanks to Martin Hjelmare for the careful review and suggestions. (All mistaken improvements and new bugs are my own.) * Removed semicolon that lint caught. * Idiomatic python improvements Use dict.values() (instead of making it easier to add local looping variable on the keys by using _, bar = ...items()) Use [] when the key is known to exist. * Use dict[key] instead of .get (incl. fixing typo). Use .values() instead of .items() when ignoring keys. * Gotta use devices.get(prefix) since we use no prefix for the singleton elk instance * fix requirement to use newer elkm1 that supports my changes for multiple elk devices * Removed spurious + between a string broken between two lines for formatting; was failing a lint check about logging needing to use %s * Remove REQUIREMENTS and DEPENDENCIES since those are now taken care of by the manifest.json file. * Add configuration check that the prefixes are all unique * Use new dependency 'getmac' to get mac address of Elk M1 controllers and use that for uniqueid if possible, else use None. Also removed some procedural checking of unique prefix since that's now handled at schema check time. * Whitespace changes to make style checker happy and code more consistent * Removed unused variable, added blank line * Make getmac a requirement not dependency I should've RTFM. * ws only change; I really need to get Emacs to understand these style guidelines * Ran script/gen_requirements_all.py; script/setup needed to be run so that was failing. * More style check fixes and one bug fix. * Incomplete set of changes from last push * More conform-to-hass-style changes: use caps to start log message (and do not use function name even for debug message. And do not use string concatenation; prefer new-style .format. * Style fixes. * Switch back to using the prefix config field for setting the unique_id since the mac address approach has numerous shortcomings including: 1) new dependency; 2) lack of reliability; 3) doesn't work for serial connections; 4) breaks when a layer 4+ networking entity intermediates the elk m1 connection. * Reran to update (removing getmac dependency) * Skipped trailing ','; keep forgetting which languages are forgiving about this practical nicety of allowing trailing commas without changing the semantics. * Validate uniqueness on lowercase versions of the prefix since we're gonna use .lower() on creating the entity id that has to be unique; do the _has_all_unique_prefixes check last so we get errors from the device schema before complaining about the uniqueness problem, if any * Use vol.Lower to convert to lowercase instead of the map. Also fixed a pair of bugs for the alarm control panel display message service -- since data templates always generate strings, the values subject to range/set restrictions need to be coerced to their proper type before the check * Fix some flake8 warnings. * Fixed typo; it's Coerce not coerce. * Use elkm1m_ string to start unique_id when and only when there is a non-empty prefix given; this enables backward compatibility to avoid a breaking change by letting the elkm1_ start to unique_id keep working exactly as it used to. * minor comment tweak to force automation tests to run again since they failed for unrelated reasons last time * There's actually been a 0.7.15 release which was meta-information and tidying only so we might as well depend on it * Forgot to update this with gen_requirements_all.py
2019-07-27 08:36:09 +00:00
entities = []
for elk_data in elk_datas.values():
2019-07-31 19:25:30 +00:00
elk = elk_data["elk"]
entities = create_elk_entities(elk_data, elk.areas, "area", ElkArea, entities)
async_add_entities(entities, True)
def _dispatch(signal, entity_ids, *args):
for entity_id in entity_ids:
async_dispatcher_send(hass, f"{signal}_{entity_id}", *args)
def _arm_service(service):
entity_ids = service.data.get(ATTR_ENTITY_ID, [])
arm_level = _arm_services().get(service.service)
args = (arm_level, service.data.get(ATTR_CODE))
_dispatch(SIGNAL_ARM_ENTITY, entity_ids, *args)
for service in _arm_services():
hass.services.async_register(
DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA
2019-07-31 19:25:30 +00:00
)
def _display_message_service(service):
entity_ids = service.data.get(ATTR_ENTITY_ID, [])
data = service.data
2019-07-31 19:25:30 +00:00
args = (
data["clear"],
data["beep"],
data["timeout"],
data["line1"],
data["line2"],
)
_dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args)
hass.services.async_register(
DOMAIN,
SERVICE_ALARM_DISPLAY_MESSAGE,
2019-07-31 19:25:30 +00:00
_display_message_service,
DISPLAY_MESSAGE_SERVICE_SCHEMA,
)
def _arm_services():
return {
SERVICE_ALARM_ARM_VACATION: ArmLevel.ARMED_VACATION.value,
SERVICE_ALARM_ARM_HOME_INSTANT: ArmLevel.ARMED_STAY_INSTANT.value,
SERVICE_ALARM_ARM_NIGHT_INSTANT: ArmLevel.ARMED_NIGHT_INSTANT.value,
}
class ElkArea(ElkEntity, AlarmControlPanel):
"""Representation of an Area / Partition within the ElkM1 alarm panel."""
def __init__(self, element, elk, elk_data):
"""Initialize Area as Alarm Control Panel."""
super().__init__(element, elk, elk_data)
2019-07-31 19:25:30 +00:00
self._changed_by_entity_id = ""
self._state = None
async def async_added_to_hass(self):
"""Register callback for ElkM1 changes."""
await super().async_added_to_hass()
for keypad in self._elk.keypads:
keypad.add_callback(self._watch_keypad)
async_dispatcher_connect(
self.hass, f"{SIGNAL_ARM_ENTITY}_{self.entity_id}", self._arm_service
2019-07-31 19:25:30 +00:00
)
async_dispatcher_connect(
2019-07-31 19:25:30 +00:00
self.hass,
f"{SIGNAL_DISPLAY_MESSAGE}_{self.entity_id}",
2019-07-31 19:25:30 +00:00
self._display_message,
)
def _watch_keypad(self, keypad, changeset):
if keypad.area != self._element.index:
return
2019-07-31 19:25:30 +00:00
if changeset.get("last_user") is not None:
self._changed_by_entity_id = self.hass.data[DOMAIN][self._prefix][
2019-07-31 19:25:30 +00:00
"keypads"
].get(keypad.index, "")
self.async_schedule_update_ha_state(True)
@property
def code_format(self):
"""Return the alarm code format."""
return FORMAT_NUMBER
@property
def state(self):
"""Return the state of the element."""
return self._state
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT
@property
def device_state_attributes(self):
"""Attributes of the area."""
attrs = self.initial_attrs()
elmt = self._element
2019-07-31 19:25:30 +00:00
attrs["is_exit"] = elmt.is_exit
attrs["timer1"] = elmt.timer1
attrs["timer2"] = elmt.timer2
if elmt.armed_status is not None:
2019-07-31 19:25:30 +00:00
attrs["armed_status"] = ArmedStatus(elmt.armed_status).name.lower()
if elmt.arm_up_state is not None:
2019-07-31 19:25:30 +00:00
attrs["arm_up_state"] = ArmUpState(elmt.arm_up_state).name.lower()
if elmt.alarm_state is not None:
2019-07-31 19:25:30 +00:00
attrs["alarm_state"] = AlarmState(elmt.alarm_state).name.lower()
attrs["changed_by_entity_id"] = self._changed_by_entity_id
return attrs
def _element_changed(self, element, changeset):
elk_state_to_hass_state = {
ArmedStatus.DISARMED.value: STATE_ALARM_DISARMED,
ArmedStatus.ARMED_AWAY.value: STATE_ALARM_ARMED_AWAY,
ArmedStatus.ARMED_STAY.value: STATE_ALARM_ARMED_HOME,
ArmedStatus.ARMED_STAY_INSTANT.value: STATE_ALARM_ARMED_HOME,
ArmedStatus.ARMED_TO_NIGHT.value: STATE_ALARM_ARMED_NIGHT,
ArmedStatus.ARMED_TO_NIGHT_INSTANT.value: STATE_ALARM_ARMED_NIGHT,
ArmedStatus.ARMED_TO_VACATION.value: STATE_ALARM_ARMED_AWAY,
}
if self._element.alarm_state is None:
self._state = None
elif self._area_is_in_alarm_state():
self._state = STATE_ALARM_TRIGGERED
elif self._entry_exit_timer_is_running():
2019-07-31 19:25:30 +00:00
self._state = (
STATE_ALARM_ARMING if self._element.is_exit else STATE_ALARM_PENDING
)
else:
self._state = elk_state_to_hass_state[self._element.armed_status]
def _entry_exit_timer_is_running(self):
return self._element.timer1 > 0 or self._element.timer2 > 0
def _area_is_in_alarm_state(self):
return self._element.alarm_state >= AlarmState.FIRE_ALARM.value
async def async_alarm_disarm(self, code=None):
"""Send disarm command."""
self._element.disarm(int(code))
async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
self._element.arm(ArmLevel.ARMED_STAY.value, int(code))
async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
self._element.arm(ArmLevel.ARMED_AWAY.value, int(code))
async def async_alarm_arm_night(self, code=None):
"""Send arm night command."""
self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code))
async def _arm_service(self, arm_level, code):
self._element.arm(arm_level, code)
async def _display_message(self, clear, beep, timeout, line1, line2):
"""Display a message on all keypads for the area."""
self._element.display_message(clear, beep, timeout, line1, line2)