-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
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 support for ElkM1 alarm/automation panel #16952
Changes from 2 commits
881d3ea
2d9f094
e882ea9
4cf93a8
a4046bf
bc308c6
1d38a2b
036d65a
690cfcc
ba84800
60571fc
5df50e1
b8ffc66
12f359d
3b5adf8
a59f4d8
a3e3bb9
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,165 @@ | ||
""" | ||
Each ElkM1 area will be created as a separate alarm_control_panel in HASS. | ||
|
||
For more details about this platform, please refer to the documentation at | ||
https://home-assistant.io/components/alarm_control_panel.elkm1/ | ||
""" | ||
|
||
import voluptuous as vol | ||
import homeassistant.helpers.config_validation as cv | ||
import homeassistant.components.alarm_control_panel as alarm | ||
from homeassistant.const import (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, STATE_UNKNOWN) | ||
|
||
from homeassistant.components.elkm1 import (DOMAIN, create_elk_devices, | ||
ElkDeviceBase, | ||
register_elk_service) | ||
from elkm1_lib.const import AlarmState, ArmedStatus, ArmLevel, ArmUpState | ||
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 won't work. Requirement library names must only be imported within functions. 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 does work. Is there a reason for importing only within functions? 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 understand the normal reasoning (that things would crash if trying to import a library not yet installed by HASS as a dependency). In this case it won't cause that problem (though it breaks with the established pattern) since trying to load the component by itself probably fails terribly. The alarm_control_panel.elkm1 (and light and so on) never get loaded unless they were loaded by the top-level elkm1 component being configured, which means the library must be installed, which makes this safe (even if not "how it's done"). Even if we made things get imported only in functions, you couldn't load alarm_control_panel by itself without configuring the elkm1 platform. If we have to move those imports into functions it means a lot of repetitive imports. If we have to, we have to, it's just annoying and ugly (and maybe impacts performance, but I imagine that after the first import the repeats are nearly zero cost). 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. 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. So what is established is:
What would have to happen to improve the rule? 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. The rule is there for a reason. We can't guard users from configuring a platform directly instead of the component. If the users would do that, there would be an error, since the platform is imported before the dependencies are loaded and requirements installed. 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. Thank-you. That helps. |
||
|
||
DEPENDENCIES = [DOMAIN] | ||
|
||
STATE_ALARM_ARMED_VACATION = 'armed_vacation' | ||
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 isn't used. |
||
STATE_ALARM_ARMED_HOME_INSTANT = 'armed_home_instant' | ||
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 isn't used. |
||
STATE_ALARM_ARMED_NIGHT_INSTANT = 'armed_night_instant' | ||
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 isn't used. 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. The extra states aren't used yet because we couldn't use them on the old regular HASS UI, without them being added to polymer, etc. We've been trying to hammer out what additional states are needed so we can get them added to HASS properly. See home-assistant/architecture#54 for more on that, if you're curious. |
||
|
||
SERVICE_TO_ELK = { | ||
'alarm_arm_vacation': 'async_alarm_arm_vacation', | ||
'alarm_arm_home_instant': 'async_alarm_arm_home_instant', | ||
'alarm_arm_night_instant': 'async_alarm_arm_night_instant', | ||
} | ||
|
||
DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema({ | ||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, | ||
vol.Optional('clear', default=2): vol.In([0, 1, 2]), | ||
vol.Optional('beep', default=False): cv.boolean, | ||
vol.Optional('timeout', default=0): vol.Range(min=0, max=65535), | ||
vol.Optional('line1', default=''): cv.string, | ||
vol.Optional('line2', default=''): cv.string, | ||
}) | ||
|
||
ELK_STATE_TO_HASS_STATE = { | ||
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. Move this to within a function. 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 there a reason why? 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. |
||
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, | ||
} | ||
|
||
|
||
# pylint: disable=unused-argument | ||
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 already globally disabled. |
||
async def async_setup_platform(hass, config, async_add_devices, | ||
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. Rename |
||
discovery_info): | ||
"""Setup the ElkM1 alarm platform.""" | ||
|
||
elk = hass.data[DOMAIN]['elk'] | ||
devices = create_elk_devices(hass, elk.areas, 'area', ElkArea, []) | ||
async_add_devices(devices, True) | ||
|
||
for service, method in SERVICE_TO_ELK.items(): | ||
register_elk_service( | ||
hass, alarm.DOMAIN, service, alarm.ALARM_SERVICE_SCHEMA, method) | ||
|
||
register_elk_service( | ||
hass, alarm.DOMAIN, 'alarm_display_message', | ||
DISPLAY_MESSAGE_SERVICE_SCHEMA, 'async_alarm_display_message') | ||
|
||
return True | ||
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. Nothing is checking this return value, so we can remove the statement. |
||
|
||
|
||
class ElkArea(ElkDeviceBase, alarm.AlarmControlPanel): | ||
"""Representation of an Area / Partition within the ElkM1 alarm panel.""" | ||
|
||
def __init__(self, device, hass, config): | ||
"""Initialize Area as Alarm Control Panel.""" | ||
ElkDeviceBase.__init__(self, 'alarm_control_panel', device, | ||
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. Use 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. Tried that before. Doesn't work. Here's the error
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. How did you try it? Did you include |
||
hass, config) | ||
self._changed_by_entity_id = '' | ||
|
||
for keypad in self._elk.keypads: | ||
keypad.add_callback(self._watch_keypad) | ||
MartinHjelmare marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def _watch_keypad(self, keypad, changeset): | ||
if keypad.area != self._element.index: | ||
return | ||
if changeset.get('last_user') is not None: | ||
self._changed_by_entity_id = self._hass.data[ | ||
DOMAIN]['keypads'].get(keypad.index, '') | ||
self.async_schedule_update_ha_state(True) | ||
|
||
@property | ||
def code_format(self): | ||
"""Return the alarm code format.""" | ||
return '^[0-9]{4}([0-9]{2})?$' | ||
|
||
@property | ||
def device_state_attributes(self): | ||
"""Attributes of the area.""" | ||
attrs = self.initial_attrs() | ||
elmt = self._element | ||
attrs['is_exit'] = elmt.is_exit | ||
attrs['timer1'] = elmt.timer1 | ||
attrs['timer2'] = elmt.timer2 | ||
attrs['armed_status'] = STATE_UNKNOWN if elmt.armed_status is None \ | ||
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. Unknown state attributes are normally not reported at all. 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. Then what happens if someone has an automation that checks the attribute, and it doesn't exist? 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. Probably use |
||
else ArmedStatus(elmt.armed_status).name.lower() | ||
attrs['arm_up_state'] = STATE_UNKNOWN if elmt.arm_up_state is None \ | ||
else ArmUpState(elmt.arm_up_state).name.lower() | ||
attrs['alarm_state'] = STATE_UNKNOWN if elmt.alarm_state is None \ | ||
else AlarmState(elmt.alarm_state).name.lower() | ||
attrs['changed_by_entity_id'] = self._changed_by_entity_id | ||
return attrs | ||
|
||
# pylint: disable=unused-argument | ||
def _element_changed(self, element, changeset): | ||
if self._element.alarm_state is None: | ||
self._state = STATE_UNKNOWN | ||
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. Use |
||
elif self._area_is_in_alarm_state(): | ||
self._state = STATE_ALARM_TRIGGERED | ||
elif self._entry_exit_timer_is_running(): | ||
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 away command.""" | ||
self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code)) | ||
|
||
async def async_alarm_arm_home_instant(self, code): | ||
"""Send arm vacation command.""" | ||
self._element.arm(ArmLevel.ARMED_STAY_INSTANT.value, int(code)) | ||
|
||
async def async_alarm_arm_night_instant(self, code): | ||
"""Send arm vacation command.""" | ||
self._element.arm(ArmLevel.ARMED_VACATION.value, int(code)) | ||
|
||
async def async_alarm_arm_vacation(self, code): | ||
"""Send arm vacation command.""" | ||
self._element.arm(ArmLevel.ARMED_VACATION.value, int(code)) | ||
|
||
async def async_alarm_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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
""" | ||
Support for control of ElkM1 connected thermostats. | ||
|
||
For more details about this platform, please refer to the documentation at | ||
https://home-assistant.io/components/climate.elkm1/ | ||
""" | ||
|
||
from homeassistant.components.climate import (ATTR_TARGET_TEMP_HIGH, | ||
ATTR_TARGET_TEMP_LOW, | ||
PRECISION_WHOLE, STATE_AUTO, | ||
STATE_COOL, STATE_FAN_ONLY, | ||
STATE_HEAT, STATE_IDLE, | ||
SUPPORT_AUX_HEAT, | ||
SUPPORT_FAN_MODE, | ||
SUPPORT_OPERATION_MODE, | ||
SUPPORT_TARGET_TEMPERATURE_HIGH, | ||
SUPPORT_TARGET_TEMPERATURE_LOW, | ||
ClimateDevice) | ||
from homeassistant.const import (STATE_ON, STATE_UNKNOWN) | ||
|
||
from homeassistant.components.elkm1 import (DOMAIN, ElkDeviceBase, | ||
create_elk_devices) | ||
from elkm1_lib.const import ThermostatFan, ThermostatMode, ThermostatSetting | ||
|
||
DEPENDENCIES = [DOMAIN] | ||
|
||
SUPPORT_FLAGS = ( | ||
SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_TARGET_TEMPERATURE_LOW | | ||
SUPPORT_OPERATION_MODE | SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT) | ||
|
||
|
||
# pylint: disable=unused-argument | ||
async def async_setup_platform(hass, config, async_add_devices, | ||
discovery_info): | ||
"""Setup the Elk switch platform.""" | ||
elk = hass.data[DOMAIN]['elk'] | ||
async_add_devices(create_elk_devices(hass, elk.thermostats, 'thermostat', | ||
ElkThermostat, []), True) | ||
return True | ||
|
||
|
||
# pylint: disable=too-many-public-methods | ||
class ElkThermostat(ElkDeviceBase, ClimateDevice): | ||
"""Elk connected thermostat as Climate device.""" | ||
def __init__(self, device, hass, config): | ||
"""Initialize Thermostat.""" | ||
ElkDeviceBase.__init__(self, 'climate', device, hass, config) | ||
|
||
# pylint: disable=unused-argument | ||
def _element_changed(self, element, changeset): | ||
pass | ||
|
||
@property | ||
def supported_features(self): | ||
"""Return the list of supported features.""" | ||
return SUPPORT_FLAGS | ||
|
||
@property | ||
def temperature_unit(self): | ||
"""Return the unit of measurement.""" | ||
return self._temperature_unit | ||
|
||
@property | ||
def unit_of_measurement(self): | ||
"""Return the unit of measurement to display.""" | ||
return self.temperature_unit | ||
|
||
@property | ||
def current_temperature(self): | ||
"""Return the current temperature.""" | ||
return self._element.current_temp | ||
|
||
@property | ||
def state(self): | ||
"""Return the current state.""" | ||
# We can't actually tell if it's actively running in any of these | ||
# modes, just what mode is set | ||
if (self._element.mode == ThermostatMode.OFF.value) and ( | ||
self._element.fan == ThermostatFan.ON.value): | ||
return STATE_FAN_ONLY | ||
if self._element.mode == ThermostatMode.OFF.value: | ||
return STATE_IDLE | ||
if (self._element.mode == ThermostatMode.HEAT.value) or ( | ||
self._element.mode == ThermostatMode.EMERGENCY_HEAT.value): | ||
return STATE_HEAT | ||
if self._element.mode == ThermostatMode.COOL.value: | ||
return STATE_COOL | ||
if self._element.mode == ThermostatMode.AUTO.value: | ||
return STATE_AUTO | ||
return STATE_UNKNOWN | ||
|
||
@property | ||
def precision(self): | ||
"""Return the precision of the system.""" | ||
return PRECISION_WHOLE | ||
|
||
@property | ||
def current_humidity(self): | ||
"""Return the current humidity.""" | ||
return self._element.humidity | ||
|
||
@property | ||
def is_aux_heat_on(self): | ||
"""Return true if aux heater.""" | ||
return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value | ||
|
||
@property | ||
def target_temperature(self): | ||
"""Return the temperature we try to reach.""" | ||
if (self._element.mode == ThermostatMode.HEAT.value) or ( | ||
self._element.mode == ThermostatMode.EMERGENCY_HEAT.value): | ||
return self._element.heat_setpoint | ||
if self._element.mode == ThermostatMode.COOL.value: | ||
return self._element.cool_setpoint | ||
return None | ||
|
||
@property | ||
def target_temperature_high(self): | ||
"""Return the highbound target temperature we try to reach.""" | ||
return self._element.cool_setpoint | ||
|
||
@property | ||
def target_temperature_low(self): | ||
"""Return the lowbound target temperature we try to reach.""" | ||
return self._element.heat_setpoint | ||
|
||
@property | ||
def min_temp(self): | ||
"""Return the minimum temp supported.""" | ||
return 1 | ||
|
||
@property | ||
def max_temp(self): | ||
"""Return the maximum temp supported.""" | ||
return 99 | ||
|
||
@property | ||
def current_operation(self): | ||
"""Return current operation ie. heat, cool, idle.""" | ||
return self.state | ||
|
||
@property | ||
def operation_list(self): | ||
"""Return the list of available operation modes.""" | ||
return [STATE_IDLE, STATE_HEAT, STATE_COOL, STATE_AUTO, STATE_FAN_ONLY] | ||
|
||
@property | ||
def target_temperature_step(self): | ||
"""Return the supported step of target temperature.""" | ||
return 1 | ||
|
||
@property | ||
def current_fan_mode(self): | ||
"""Return the fan setting.""" | ||
if self._element.fan == ThermostatFan.AUTO.value: | ||
return STATE_AUTO | ||
if self._element.fan == ThermostatFan.ON.value: | ||
return STATE_ON | ||
return STATE_UNKNOWN | ||
|
||
def set_operation_mode(self, operation_mode): | ||
"""Set mode.""" | ||
if operation_mode == STATE_IDLE: | ||
self._element.set(ThermostatSetting.MODE.value, | ||
ThermostatMode.OFF.value) | ||
self._element.set(ThermostatSetting.FAN.value, | ||
ThermostatFan.AUTO.value) | ||
elif operation_mode == STATE_HEAT: | ||
self._element.set(ThermostatSetting.MODE.value, | ||
ThermostatMode.HEAT.value) | ||
self._element.set(ThermostatSetting.FAN.value, | ||
ThermostatFan.AUTO.value) | ||
elif operation_mode == STATE_COOL: | ||
self._element.set(ThermostatSetting.MODE.value, | ||
ThermostatMode.COOL.value) | ||
self._element.set(ThermostatSetting.FAN.value, | ||
ThermostatFan.AUTO.value) | ||
elif operation_mode == STATE_AUTO: | ||
self._element.set(ThermostatSetting.MODE.value, | ||
ThermostatMode.AUTO.value) | ||
self._element.set(ThermostatSetting.FAN.value, | ||
ThermostatFan.AUTO.value) | ||
elif operation_mode == STATE_FAN_ONLY: | ||
self._element.set(ThermostatSetting.MODE.value, | ||
ThermostatMode.OFF.value) | ||
self._element.set(ThermostatSetting.FAN.value, | ||
ThermostatFan.ON.value) | ||
|
||
def turn_aux_heat_on(self): | ||
"""Turn auxiliary heater on.""" | ||
self._element.set(ThermostatSetting.MODE.value, | ||
ThermostatMode.EMERGENCY_HEAT.value) | ||
self._element.set(ThermostatSetting.FAN.value, | ||
ThermostatFan.AUTO.value) | ||
|
||
def turn_aux_heat_off(self): | ||
"""Turn auxiliary heater off.""" | ||
self._element.set(ThermostatSetting.MODE.value, | ||
ThermostatMode.HEAT.value) | ||
self._element.set(ThermostatSetting.FAN.value, | ||
ThermostatFan.AUTO.value) | ||
|
||
@property | ||
def fan_list(self): | ||
"""Return the list of available fan modes.""" | ||
return [STATE_AUTO, STATE_ON] | ||
|
||
def set_fan_mode(self, fan_mode): | ||
"""Set new target fan mode.""" | ||
if fan_mode == STATE_AUTO: | ||
self._element.set(ThermostatSetting.FAN.value, | ||
ThermostatFan.AUTO.value) | ||
elif fan_mode == STATE_ON: | ||
self._element.set(ThermostatSetting.FAN.value, | ||
ThermostatFan.ON.value) | ||
|
||
def set_temperature(self, **kwargs): | ||
"""Set new target temperature.""" | ||
low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) | ||
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) | ||
if low_temp is not None: | ||
low_temp = round(low_temp) | ||
self._element.set(ThermostatSetting.HEAT_SETPOINT.value, low_temp) | ||
if high_temp is not None: | ||
high_temp = round(high_temp) | ||
self._element.set(ThermostatSetting.COOL_SETPOINT.value, high_temp) |
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.
Import the elkm1 domain as another name. This platform belongs to the alarm_control_panel domain.