core/homeassistant/components/elkm1/sensor.py

274 lines
9.2 KiB
Python
Raw Normal View History

"""Support for control of ElkM1 sensors."""
from elkm1_lib.const import (
SettingFormat,
ZoneLogicalStatus,
ZonePhysicalStatus,
ZoneType,
)
from elkm1_lib.util import pretty_const, username
import voluptuous as vol
from homeassistant.components.sensor import SensorEntity
2021-10-25 07:00:06 +00:00
from homeassistant.const import ELECTRIC_POTENTIAL_VOLT, ENTITY_CATEGORY_DIAGNOSTIC
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_platform
from . import ElkAttachedEntity, create_elk_entities
from .const import ATTR_VALUE, DOMAIN, ELK_USER_CODE_SERVICE_SCHEMA
SERVICE_SENSOR_COUNTER_REFRESH = "sensor_counter_refresh"
SERVICE_SENSOR_COUNTER_SET = "sensor_counter_set"
SERVICE_SENSOR_ZONE_BYPASS = "sensor_zone_bypass"
SERVICE_SENSOR_ZONE_TRIGGER = "sensor_zone_trigger"
UNDEFINED_TEMPATURE = -40
ELK_SET_COUNTER_SERVICE_SCHEMA = {
vol.Required(ATTR_VALUE): vol.All(vol.Coerce(int), vol.Range(0, 65535))
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Create the Elk-M1 sensor platform."""
elk_data = hass.data[DOMAIN][config_entry.entry_id]
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 = []
elk = elk_data["elk"]
create_elk_entities(elk_data, elk.counters, "counter", ElkCounter, entities)
create_elk_entities(elk_data, elk.keypads, "keypad", ElkKeypad, entities)
create_elk_entities(elk_data, [elk.panel], "panel", ElkPanel, entities)
create_elk_entities(elk_data, elk.settings, "setting", ElkSetting, entities)
create_elk_entities(elk_data, elk.zones, "zone", ElkZone, entities)
async_add_entities(entities, True)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_SENSOR_COUNTER_REFRESH,
{},
"async_counter_refresh",
)
platform.async_register_entity_service(
SERVICE_SENSOR_COUNTER_SET,
ELK_SET_COUNTER_SERVICE_SCHEMA,
"async_counter_set",
)
platform.async_register_entity_service(
SERVICE_SENSOR_ZONE_BYPASS,
ELK_USER_CODE_SERVICE_SCHEMA,
"async_zone_bypass",
)
platform.async_register_entity_service(
SERVICE_SENSOR_ZONE_TRIGGER,
{},
"async_zone_trigger",
)
def temperature_to_state(temperature, undefined_temperature):
"""Convert temperature to a state."""
return temperature if temperature > undefined_temperature else None
class ElkSensor(ElkAttachedEntity, SensorEntity):
"""Base representation of Elk-M1 sensor."""
def __init__(self, element, elk, elk_data):
"""Initialize the base of all Elk sensors."""
super().__init__(element, elk, elk_data)
self._state = None
@property
def native_value(self):
"""Return the state of the sensor."""
return self._state
async def async_counter_refresh(self):
"""Refresh the value of a counter from the panel."""
if not isinstance(self, ElkCounter):
raise HomeAssistantError("supported only on ElkM1 Counter sensors")
self._element.get()
async def async_counter_set(self, value=None):
"""Set the value of a counter on the panel."""
if not isinstance(self, ElkCounter):
raise HomeAssistantError("supported only on ElkM1 Counter sensors")
self._element.set(value)
async def async_zone_bypass(self, code=None):
"""Bypass zone."""
if not isinstance(self, ElkZone):
raise HomeAssistantError("supported only on ElkM1 Zone sensors")
self._element.bypass(code)
async def async_zone_trigger(self):
"""Trigger zone."""
if not isinstance(self, ElkZone):
raise HomeAssistantError("supported only on ElkM1 Zone sensors")
self._element.trigger()
class ElkCounter(ElkSensor):
"""Representation of an Elk-M1 Counter."""
@property
def icon(self):
"""Icon to use in the frontend."""
2019-07-31 19:25:30 +00:00
return "mdi:numeric"
def _element_changed(self, element, changeset):
self._state = self._element.value
class ElkKeypad(ElkSensor):
"""Representation of an Elk-M1 Keypad."""
@property
def temperature_unit(self):
"""Return the temperature unit."""
return self._temperature_unit
@property
def native_unit_of_measurement(self):
"""Return the unit of measurement."""
return self._temperature_unit
@property
def icon(self):
"""Icon to use in the frontend."""
2019-07-31 19:25:30 +00:00
return "mdi:thermometer-lines"
@property
def extra_state_attributes(self):
"""Attributes of the sensor."""
attrs = self.initial_attrs()
2019-07-31 19:25:30 +00:00
attrs["area"] = self._element.area + 1
attrs["temperature"] = self._state
2019-07-31 19:25:30 +00:00
attrs["last_user_time"] = self._element.last_user_time.isoformat()
attrs["last_user"] = self._element.last_user + 1
attrs["code"] = self._element.code
attrs["last_user_name"] = username(self._elk, self._element.last_user)
attrs["last_keypress"] = self._element.last_keypress
return attrs
def _element_changed(self, element, changeset):
self._state = temperature_to_state(
self._element.temperature, UNDEFINED_TEMPATURE
)
class ElkPanel(ElkSensor):
"""Representation of an Elk-M1 Panel."""
2021-10-25 07:00:06 +00:00
_attr_entity_category = ENTITY_CATEGORY_DIAGNOSTIC
@property
def icon(self):
"""Icon to use in the frontend."""
return "mdi:home"
@property
def extra_state_attributes(self):
"""Attributes of the sensor."""
attrs = self.initial_attrs()
2019-07-31 19:25:30 +00:00
attrs["system_trouble_status"] = self._element.system_trouble_status
return attrs
def _element_changed(self, element, changeset):
if self._elk.is_connected():
2019-07-31 19:25:30 +00:00
self._state = (
"Paused" if self._element.remote_programming_status else "Connected"
)
else:
2019-07-31 19:25:30 +00:00
self._state = "Disconnected"
class ElkSetting(ElkSensor):
"""Representation of an Elk-M1 Setting."""
@property
def icon(self):
"""Icon to use in the frontend."""
2019-07-31 19:25:30 +00:00
return "mdi:numeric"
def _element_changed(self, element, changeset):
self._state = self._element.value
@property
def extra_state_attributes(self):
"""Attributes of the sensor."""
attrs = self.initial_attrs()
2019-07-31 19:25:30 +00:00
attrs["value_format"] = SettingFormat(self._element.value_format).name.lower()
return attrs
class ElkZone(ElkSensor):
"""Representation of an Elk-M1 Zone."""
@property
def icon(self):
"""Icon to use in the frontend."""
zone_icons = {
2019-07-31 19:25:30 +00:00
ZoneType.FIRE_ALARM.value: "fire",
ZoneType.FIRE_VERIFIED.value: "fire",
ZoneType.FIRE_SUPERVISORY.value: "fire",
ZoneType.KEYFOB.value: "key",
ZoneType.NON_ALARM.value: "alarm-off",
ZoneType.MEDICAL_ALARM.value: "medical-bag",
ZoneType.POLICE_ALARM.value: "alarm-light",
ZoneType.POLICE_NO_INDICATION.value: "alarm-light",
ZoneType.KEY_MOMENTARY_ARM_DISARM.value: "power",
ZoneType.KEY_MOMENTARY_ARM_AWAY.value: "power",
ZoneType.KEY_MOMENTARY_ARM_STAY.value: "power",
ZoneType.KEY_MOMENTARY_DISARM.value: "power",
ZoneType.KEY_ON_OFF.value: "toggle-switch",
ZoneType.MUTE_AUDIBLES.value: "volume-mute",
ZoneType.POWER_SUPERVISORY.value: "power-plug",
ZoneType.TEMPERATURE.value: "thermometer-lines",
ZoneType.ANALOG_ZONE.value: "speedometer",
ZoneType.PHONE_KEY.value: "phone-classic",
ZoneType.INTERCOM_KEY.value: "deskphone",
}
return f"mdi:{zone_icons.get(self._element.definition, 'alarm-bell')}"
@property
def extra_state_attributes(self):
"""Attributes of the sensor."""
attrs = self.initial_attrs()
2019-07-31 19:25:30 +00:00
attrs["physical_status"] = ZonePhysicalStatus(
self._element.physical_status
).name.lower()
attrs["logical_status"] = ZoneLogicalStatus(
self._element.logical_status
).name.lower()
attrs["definition"] = ZoneType(self._element.definition).name.lower()
attrs["area"] = self._element.area + 1
attrs["triggered_alarm"] = self._element.triggered_alarm
return attrs
@property
def temperature_unit(self):
"""Return the temperature unit."""
if self._element.definition == ZoneType.TEMPERATURE.value:
return self._temperature_unit
return None
@property
def native_unit_of_measurement(self):
"""Return the unit of measurement."""
if self._element.definition == ZoneType.TEMPERATURE.value:
return self._temperature_unit
if self._element.definition == ZoneType.ANALOG_ZONE.value:
return ELECTRIC_POTENTIAL_VOLT
return None
def _element_changed(self, element, changeset):
if self._element.definition == ZoneType.TEMPERATURE.value:
self._state = temperature_to_state(
self._element.temperature, UNDEFINED_TEMPATURE
)
elif self._element.definition == ZoneType.ANALOG_ZONE.value:
self._state = self._element.voltage
else:
2019-07-31 19:25:30 +00:00
self._state = pretty_const(
ZoneLogicalStatus(self._element.logical_status).name
)