-
-
Notifications
You must be signed in to change notification settings - Fork 38.2k
Add Elk-M1 sensor platform #17342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Elk-M1 sensor platform #17342
Changes from 3 commits
3730e09
de02584
265c10e
7b2a4ad
c4edb88
209da24
1083cfb
76722e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,315 @@ | ||
| """ | ||
| Support for control of ElkM1 sensors. | ||
|
|
||
| On the ElkM1 there are 6 types of sensors: | ||
| - Counters that are integers that can be read | ||
| - Keypads that have temperature (not all models, but no way to know) | ||
| - Panel that monitors the state of the connection, versions, etc. | ||
| - Settings that are used to trigger Elk-M1 automations | ||
| - Thermostats that have temperature | ||
| - Zones that are multi-state, voltage, or temperature. | ||
|
|
||
| For more details about this platform, please refer to the documentation at | ||
| https://home-assistant.io/components/sensor.elkm1/ | ||
| """ | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove blank line.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is still left. |
||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.components.elkm1 import ( | ||
| DOMAIN as ELK_DOMAIN, create_elk_entities, ElkEntity) | ||
| from homeassistant.components.sensor import DOMAIN | ||
| from homeassistant.const import ATTR_ENTITY_ID | ||
| import homeassistant.helpers.config_validation as cv | ||
| from homeassistant.helpers.dispatcher import ( | ||
| async_dispatcher_connect, async_dispatcher_send) | ||
|
|
||
| DEPENDENCIES = [ELK_DOMAIN] | ||
|
|
||
| SPEAK_SERVICE_SCHEMA = vol.Schema({ | ||
| vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, | ||
| vol.Required('number'): vol.Range(min=0, max=999), | ||
| }) | ||
|
|
||
| SIGNAL_SPEAK_WORD = 'elkm1_speak_word' | ||
| SIGNAL_SPEAK_PHRASE = 'elkm1_speak_phrase' | ||
|
|
||
|
|
||
| async def async_setup_platform(hass, config, async_add_entities, | ||
| discovery_info=None): | ||
| """Create the Elk-M1 sensor platform.""" | ||
| if discovery_info is None: | ||
| return | ||
|
|
||
| elk = hass.data[ELK_DOMAIN]['elk'] | ||
| entities = create_elk_entities( | ||
| hass, elk.counters, 'counter', ElkCounter, []) | ||
| entities = create_elk_entities( | ||
| hass, elk.keypads, 'keypad', ElkKeypad, entities) | ||
| entities = create_elk_entities( | ||
| hass, [elk.panel], 'panel', ElkPanel, entities) | ||
| entities = create_elk_entities( | ||
| hass, elk.settings, 'setting', ElkSetting, entities) | ||
| entities = create_elk_entities( | ||
| hass, elk.thermostats, 'thermostat', ElkThermostat, entities) | ||
| entities = create_elk_entities( | ||
| hass, elk.zones, 'zone', ElkZone, entities) | ||
| async_add_entities(entities, True) | ||
|
|
||
| def _dispatch(signal, entity_ids, *args): | ||
| for entity_id in entity_ids: | ||
| async_dispatcher_send( | ||
| hass, '{}_{}'.format(signal, entity_id), *args) | ||
|
|
||
| def _speak_word_service(service): | ||
| entity_ids = service.data.get(ATTR_ENTITY_ID, []) | ||
| args = (service.data.get('number')) | ||
| _dispatch(SIGNAL_SPEAK_WORD, entity_ids, *args) | ||
|
|
||
| def _speak_phrase_service(service): | ||
| entity_ids = service.data.get(ATTR_ENTITY_ID, []) | ||
| args = (service.data.get('number')) | ||
| _dispatch(SIGNAL_SPEAK_PHRASE, entity_ids, *args) | ||
|
|
||
| hass.services.async_register(DOMAIN, 'elkm1_sensor_speak_word', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sensors are up until now read only, so we have never implemented any services for these platforms. Probably move the services to the elkm1 component. This is an architecture question. I don't really see why these services belong to the sensor platform in the first place. "speak word" and "speak phrase" doesn't sound like something a sensor would do. More like a speaker. But I don't know this integration.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can move the services. They seemed to be a better fit against the alarm panel rather than the entire config but serious don't know or care much as long as they can be easily invoked.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest moving them to the component.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When you say move to the component, is that
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes! |
||
| _speak_word_service, SPEAK_SERVICE_SCHEMA) | ||
| hass.services.async_register(DOMAIN, 'elkm1_sensor_speak_phrase', | ||
| _speak_phrase_service, SPEAK_SERVICE_SCHEMA) | ||
|
|
||
|
|
||
| def temperature_to_state(temperature, undefined_temperature): | ||
| """Convert temperature to a state.""" | ||
| return temperature if temperature > undefined_temperature else None | ||
|
|
||
|
|
||
| class ElkSensor(ElkEntity): | ||
| """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 state(self): | ||
| """Return the state of the sensor.""" | ||
| return self._state | ||
|
|
||
|
|
||
| class ElkCounter(ElkSensor): | ||
| """Representation of an Elk-M1 Counter.""" | ||
|
|
||
| @property | ||
| def icon(self): | ||
| """Icon to use in the frontend.""" | ||
| 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 unit_of_measurement(self): | ||
| """Return the unit of measurement.""" | ||
| return self._temperature_unit | ||
|
|
||
| @property | ||
| def icon(self): | ||
| """Icon to use in the frontend.""" | ||
| return 'mdi:thermometer-lines' | ||
|
|
||
| @property | ||
| def device_state_attributes(self): | ||
| """Attributes of the sensor.""" | ||
| from elkm1_lib.util import username | ||
|
|
||
| attrs = self.initial_attrs() | ||
| attrs['area'] = self._element.area + 1 | ||
| attrs['temperature'] = self._element.temperature | ||
| 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) | ||
| return attrs | ||
|
|
||
| def _element_changed(self, element, changeset): | ||
| self._state = temperature_to_state(self._element.temperature, -40) | ||
|
|
||
| async def async_added_to_hass(self): | ||
| """Register callback for ElkM1 changes and update entity state.""" | ||
| await super().async_added_to_hass() | ||
| self.hass.data[ELK_DOMAIN]['keypads'][ | ||
| self._element.index] = self.entity_id | ||
|
|
||
|
|
||
| class ElkPanel(ElkSensor): | ||
| """Representation of an Elk-M1 Panel.""" | ||
|
|
||
| async def async_added_to_hass(self): | ||
| """Register callback for ElkM1 changes.""" | ||
| await super().async_added_to_hass() | ||
| async_dispatcher_connect( | ||
| self.hass, '{}_{}'.format(SIGNAL_SPEAK_WORD, self.entity_id), | ||
| self._speak_word) | ||
| async_dispatcher_connect( | ||
| self.hass, '{}_{}'.format(SIGNAL_SPEAK_PHRASE, self.entity_id), | ||
| self._speak_phrase) | ||
|
|
||
| @property | ||
| def icon(self): | ||
| """Icon to use in the frontend.""" | ||
| return "mdi:home" | ||
|
|
||
| @property | ||
| def device_state_attributes(self): | ||
| """Attributes of the sensor.""" | ||
| attrs = self.initial_attrs() | ||
| attrs['elkm1_version'] = self._element.elkm1_version | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Firmware version and similar info should go in device info, not state attributes. State attributes should hold data connected to the state of the device. https://developers.home-assistant.io/docs/en/device_registry_index.html
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've read those docs and was trying to figure out how it could be used with ElkM1. Not very clear at all how to do that integration. Some examples would be helpful in the docs. So what to do? I'd like to keep the versions, implementing something else way over the top (to use your words "not super easy to implement"). What about the state associated with this entity? It tracks the state of the connection from hass to the actual platform. Is that in
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We make a distinction between dynamic info and static info. Dynamic info belongs in state and state attributes as long as it pertains to the state of a device. Static info is... static so doesn't have to do with state of the device. Static info like version has to be removed from state attributes. If the connection has multiple states it can be in, then it fits as sensor state. |
||
| attrs['system_trouble_status'] = self._element.system_trouble_status | ||
| attrs['xep_version'] = self._element.xep_version | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is xep_version?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is the version of the Ethernet controller.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, then it has to be removed too. |
||
| return attrs | ||
|
|
||
| def _element_changed(self, element, changeset): | ||
| if self._elk.is_connected(): | ||
| self._state = 'Paused' if self._element.remote_programming_status \ | ||
| else 'Connected' | ||
| else: | ||
| self._state = 'Disconnected' | ||
|
|
||
| async def _speak_word(self, number): | ||
| """Speak a word on the panel.""" | ||
| self._element.speak_word(number) | ||
|
|
||
| async def _speak_phrase(self, number): | ||
| """Speak a phrase on the panel.""" | ||
| self._element.speak_phrase(number) | ||
|
|
||
|
|
||
| class ElkSetting(ElkSensor): | ||
| """Representation of an Elk-M1 Setting.""" | ||
|
|
||
| @property | ||
| def icon(self): | ||
| """Icon to use in the frontend.""" | ||
| return 'mdi:numeric' | ||
|
|
||
| def _element_changed(self, element, changeset): | ||
| self._state = self._element.value | ||
|
|
||
| @property | ||
| def device_state_attributes(self): | ||
| """Attributes of the sensor.""" | ||
| from elkm1_lib.const import SettingFormat | ||
| attrs = self.initial_attrs() | ||
| attrs['value_format'] = SettingFormat( | ||
| self._element.value_format).name.lower() | ||
| return attrs | ||
|
|
||
|
|
||
| class ElkThermostat(ElkSensor): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a pure read only integration, no temperature control? It should be a sensor and not a climate entity?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm sure I read, but cannot find right now, that each of the sensors of a device should be represented as separate entities. Thus this thermostat entity. Perhaps @BioSehnsucht can comment as I'm guessing he read the same thing from which code followed. The next PR is for
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this thermostat reading is the same device feature that will go in the climate entity, yes then remove this sensor type.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TL;DR : I think it's fine to remove the ElkThermostat sensors for now. When we get Omni protocol re-implemented to support the additional sensors, we can add those additional sensors back in. At one time I had implemented the necessary protocol-within-a-protocol to be able to gather additional temperature sensors from Elk supported climate devices (specifically HAI Omnistat devices) which could have up to 4 temperature sensors in total (one of which was the thermostat itself). However at the moment we haven't re-implemented that again after switching from my library to @gwww 's so thermostat sensors are redundant, and once we re-implement the protocol inception necessary to support it, we can revisit and add just the 2nd-4th temp sensors as individual sensors. (In actuality, at the time, the thermostat sensor that was part of the climate entity was duplicated as a sensor because reasons ... so I always had duplicated sensors for climate) |
||
| """Representation of an Elk-M1 Thermostat.""" | ||
|
|
||
| @property | ||
| def temperature_unit(self): | ||
| """Return the temperature unit.""" | ||
| return self._temperature_unit | ||
|
|
||
| @property | ||
| def unit_of_measurement(self): | ||
| """Return the unit of measurement.""" | ||
| return self._temperature_unit | ||
|
|
||
| @property | ||
| def icon(self): | ||
| """Icon to use in the frontend.""" | ||
| return 'mdi:thermometer-lines' | ||
|
|
||
| def _element_changed(self, element, changeset): | ||
| self._state = temperature_to_state(self._element.current_temp, 0) | ||
|
|
||
|
|
||
| class ElkZone(ElkSensor): | ||
| """Representation of an Elk-M1 Zone.""" | ||
|
|
||
| @property | ||
| def icon(self): | ||
| """Icon to use in the frontend.""" | ||
| from elkm1_lib.const import ZoneType | ||
| zone_icons = { | ||
| 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 'mdi:{}'.format( | ||
| zone_icons.get(self._element.definition, 'alarm-bell')) | ||
|
|
||
| @property | ||
| def device_state_attributes(self): | ||
| """Attributes of the sensor.""" | ||
| from elkm1_lib.const import ( | ||
| ZoneLogicalStatus, ZonePhysicalStatus, ZoneType) | ||
|
|
||
| attrs = self.initial_attrs() | ||
| 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['bypassed'] = self._element.bypassed | ||
| attrs['triggered_alarm'] = self._element.triggered_alarm | ||
| return attrs | ||
|
|
||
| @property | ||
| def temperature_unit(self): | ||
| """Return the temperature unit.""" | ||
| from elkm1_lib.const import ZoneType | ||
| if self._element.definition == ZoneType.TEMPERATURE.value: | ||
| return self._temperature_unit | ||
| return None | ||
|
|
||
| @property | ||
| def unit_of_measurement(self): | ||
| """Return the unit of measurement.""" | ||
| from elkm1_lib.const import ZoneType | ||
| if self._element.definition == ZoneType.TEMPERATURE.value: | ||
| return self._temperature_unit | ||
| if self._element.definition == ZoneType.ANALOG_ZONE.value: | ||
| return 'V' | ||
| return None | ||
|
|
||
| def _element_changed(self, element, changeset): | ||
| from elkm1_lib.const import ZoneLogicalStatus, ZoneType | ||
| from elkm1_lib.util import pretty_const | ||
|
|
||
| if self._element.definition == ZoneType.TEMPERATURE.value: | ||
| self._state = temperature_to_state(self._element.temperature, -60) | ||
| elif self._element.definition == ZoneType.ANALOG_ZONE.value: | ||
| self._state = self._element.voltage | ||
| else: | ||
| self._state = pretty_const(ZoneLogicalStatus( | ||
|
MartinHjelmare marked this conversation as resolved.
|
||
| self._element.logical_status).name) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Describes the format for available ElkM1 alarm control panel services | ||
|
|
||
| elkm1_sensor_speak_word: | ||
| description: Speak a word. See list of words in ElkM1 ASCII Protocol documentation. | ||
| fields: | ||
| entity_id: | ||
| description: Name of panel to speak word on. | ||
| example: 'sensor.elkm1_panel_001' | ||
| number: | ||
| description: Word number to speak. | ||
| example: 458 | ||
|
|
||
| elkm1_sensor_speak_phrase: | ||
| description: Speak a phrase. See list of phrases in ElkM1 ASCII Protocol documentation. | ||
| fields: | ||
| entity_id: | ||
| description: Name of panel to speak phrase on. | ||
| example: 'sensor.elkm1_panel_001' | ||
| number: | ||
| description: Phrase number to speak. | ||
| example: 42 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this to the docs.