From c16c604ff42d8a5d9b6407c2f85a62da0ab86a39 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Wed, 22 Nov 2017 01:00:09 +0100 Subject: [PATCH 1/8] alarm/manual_mqtt: sync different test with alarm/manual Make sure that both tests try the same scenarios, so that the same changes can be applied to both. --- .../alarm_control_panel/test_manual_mqtt.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/tests/components/alarm_control_panel/test_manual_mqtt.py b/tests/components/alarm_control_panel/test_manual_mqtt.py index e56b6865e6e7b..1e1dfb04ad120 100644 --- a/tests/components/alarm_control_panel/test_manual_mqtt.py +++ b/tests/components/alarm_control_panel/test_manual_mqtt.py @@ -674,21 +674,6 @@ def test_trigger_with_specific_pending(self): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_home(self.hass) - self.hass.block_till_done() - - self.assertEqual(STATE_ALARM_PENDING, - self.hass.states.get(entity_id).state) - - future = dt_util.utcnow() + timedelta(seconds=10) - with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' - 'dt_util.utcnow'), return_value=future): - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - self.assertEqual(STATE_ALARM_ARMED_HOME, - self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) self.hass.block_till_done() @@ -710,7 +695,7 @@ def test_trigger_with_specific_pending(self): fire_time_changed(self.hass, future) self.hass.block_till_done() - self.assertEqual(STATE_ALARM_ARMED_HOME, + self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) def test_arm_home_via_command_topic(self): From e39173263b3cce90cc3b7962c47d45f3d5bdc41f Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Tue, 21 Nov 2017 15:33:03 +0100 Subject: [PATCH 2/8] alarm/manual, alarm/manual_mqtt: fix doc comment --- homeassistant/components/alarm_control_panel/manual.py | 6 +++--- homeassistant/components/alarm_control_panel/manual_mqtt.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 55f3834c06ab1..dfbc9ee52985b 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -86,9 +86,9 @@ class ManualAlarm(alarm.AlarmControlPanel): Representation of an alarm status. When armed, will be pending for 'pending_time', after that armed. - When triggered, will be pending for 'trigger_time'. After that will be - triggered for 'trigger_time', after that we return to the previous state - or disarm if `disarm_after_trigger` is true. + When triggered, will be pending for the triggered state's 'pending_time'. + After that will be triggered for 'trigger_time', after that we return to + the previous state or disarm if `disarm_after_trigger` is true. """ def __init__(self, hass, name, code, pending_time, trigger_time, diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 44247616b59ff..f66d7d76da8e9 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -111,9 +111,9 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): Representation of an alarm status. When armed, will be pending for 'pending_time', after that armed. - When triggered, will be pending for 'trigger_time'. After that will be - triggered for 'trigger_time', after that we return to the previous state - or disarm if `disarm_after_trigger` is true. + When triggered, will be pending for the triggered state's 'pending_time'. + After that will be triggered for 'trigger_time', after that we return to + the previous state or disarm if `disarm_after_trigger` is true. """ def __init__(self, hass, name, code, pending_time, From 6fb6788bbad6633597aaaecfb0debb4fb23a20bd Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Wed, 22 Nov 2017 00:52:07 +0100 Subject: [PATCH 3/8] alarm/manual, alarm/manual_mqtt: add pre_pending_state attribute Currently it's not clear what prevents a switch from a state "to itself" while the transition is pending. Extend the _pre_trigger_state infrastructure to avoid that case, and expose it as a new attribute for users and tests. It will also come in handy when implementing per-state delay and trigger times. Tests are added in a later patch (currently, tests do not cover post_pending_state either). Signed-off-by: Paolo Bonzini --- .../components/alarm_control_panel/manual.py | 12 ++++++++---- .../components/alarm_control_panel/manual_mqtt.py | 12 ++++++++---- tests/components/alarm_control_panel/test_manual.py | 7 +++++++ .../alarm_control_panel/test_manual_mqtt.py | 7 +++++++ 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index dfbc9ee52985b..e126074e60744 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -29,6 +29,7 @@ STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, STATE_ALARM_ARMED_CUSTOM_BYPASS] +ATTR_PRE_PENDING_STATE = 'pre_pending_state' ATTR_POST_PENDING_STATE = 'post_pending_state' @@ -100,7 +101,7 @@ def __init__(self, hass, name, code, pending_time, trigger_time, self._code = str(code) if code else None self._trigger_time = datetime.timedelta(seconds=trigger_time) self._disarm_after_trigger = disarm_after_trigger - self._pre_trigger_state = self._state + self._previous_state = self._state self._state_ts = None self._pending_time_by_state = {} @@ -129,7 +130,7 @@ def state(self): if self._disarm_after_trigger: return STATE_ALARM_DISARMED else: - self._state = self._pre_trigger_state + self._state = self._previous_state return self._state if self._state in SUPPORTED_PENDING_STATES and \ @@ -186,11 +187,13 @@ def alarm_arm_custom_bypass(self, code=None): def alarm_trigger(self, code=None): """Send alarm trigger command. No code needed.""" - self._pre_trigger_state = self._state - self._update_state(STATE_ALARM_TRIGGERED) def _update_state(self, state): + if self._state == state: + return + + self._previous_state = self._state self._state = state self._state_ts = dt_util.utcnow() self.schedule_update_ha_state() @@ -223,6 +226,7 @@ def device_state_attributes(self): state_attr = {} if self.state == STATE_ALARM_PENDING: + state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state state_attr[ATTR_POST_PENDING_STATE] = self._state return state_attr diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index f66d7d76da8e9..965af9bcd0752 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -43,6 +43,7 @@ SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED] +ATTR_PRE_PENDING_STATE = 'pre_pending_state' ATTR_POST_PENDING_STATE = 'post_pending_state' @@ -129,7 +130,7 @@ def __init__(self, hass, name, code, pending_time, self._pending_time = datetime.timedelta(seconds=pending_time) self._trigger_time = datetime.timedelta(seconds=trigger_time) self._disarm_after_trigger = disarm_after_trigger - self._pre_trigger_state = self._state + self._previous_state = self._state self._state_ts = None self._pending_time_by_state = {} @@ -166,7 +167,7 @@ def state(self): if self._disarm_after_trigger: return STATE_ALARM_DISARMED else: - self._state = self._pre_trigger_state + self._state = self._previous_state return self._state if self._state in SUPPORTED_PENDING_STATES and \ @@ -216,11 +217,13 @@ def alarm_arm_night(self, code=None): def alarm_trigger(self, code=None): """Send alarm trigger command. No code needed.""" - self._pre_trigger_state = self._state - self._update_state(STATE_ALARM_TRIGGERED) def _update_state(self, state): + if self._state == state: + return + + self._previous_state = self._state self._state = state self._state_ts = dt_util.utcnow() self.schedule_update_ha_state() @@ -253,6 +256,7 @@ def device_state_attributes(self): state_attr = {} if self.state == STATE_ALARM_PENDING: + state_attr[ATTR_PRE_PENDING_STATE] = self._previous_state state_attr[ATTR_POST_PENDING_STATE] = self._state return state_attr diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py index d65568b08447e..9cc5e673cd79d 100644 --- a/tests/components/alarm_control_panel/test_manual.py +++ b/tests/components/alarm_control_panel/test_manual.py @@ -257,6 +257,13 @@ def test_arm_night_with_pending(self): state = self.hass.states.get(entity_id) assert state.state == STATE_ALARM_ARMED_NIGHT + # Do not go to the pending state when updating to the same state + alarm_control_panel.alarm_arm_night(self.hass, CODE, entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_NIGHT, + self.hass.states.get(entity_id).state) + def test_arm_night_with_invalid_code(self): """Attempt to night home without a valid code.""" self.assertTrue(setup_component( diff --git a/tests/components/alarm_control_panel/test_manual_mqtt.py b/tests/components/alarm_control_panel/test_manual_mqtt.py index 1e1dfb04ad120..0d79e85287788 100644 --- a/tests/components/alarm_control_panel/test_manual_mqtt.py +++ b/tests/components/alarm_control_panel/test_manual_mqtt.py @@ -287,6 +287,13 @@ def test_arm_night_with_pending(self): self.assertEqual(STATE_ALARM_ARMED_NIGHT, self.hass.states.get(entity_id).state) + # Do not go to the pending state when updating to the same state + alarm_control_panel.alarm_arm_night(self.hass, CODE, entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_NIGHT, + self.hass.states.get(entity_id).state) + def test_arm_night_with_invalid_code(self): """Attempt to arm night without a valid code.""" self.assertTrue(setup_component( From 15192c2ee9ef2ab9c0e9c6d261de28f46b3c0f71 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Tue, 21 Nov 2017 15:33:03 +0100 Subject: [PATCH 4/8] alarm/manual, alarm/manual_mqtt: cleanup pending_time Change it to cv.time_period, remove it from ManualAlarm constructor since it is always filled in for each individual alarm state. --- .../components/alarm_control_panel/demo.py | 13 +++++++------ .../components/alarm_control_panel/manual.py | 16 +++++++--------- .../alarm_control_panel/manual_mqtt.py | 17 +++++++---------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/demo.py b/homeassistant/components/alarm_control_panel/demo.py index aa90fe1f88976..ba7419dffedc4 100644 --- a/homeassistant/components/alarm_control_panel/demo.py +++ b/homeassistant/components/alarm_control_panel/demo.py @@ -4,6 +4,7 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ +import datetime import homeassistant.components.alarm_control_panel.manual as manual from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, @@ -13,21 +14,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo alarm control panel platform.""" add_devices([ - manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10, False, { + manual.ManualAlarm(hass, 'Alarm', '1234', 10, False, { STATE_ALARM_ARMED_AWAY: { - CONF_PENDING_TIME: 5 + CONF_PENDING_TIME: datetime.timedelta(seconds=5), }, STATE_ALARM_ARMED_HOME: { - CONF_PENDING_TIME: 5 + CONF_PENDING_TIME: datetime.timedelta(seconds=5), }, STATE_ALARM_ARMED_NIGHT: { - CONF_PENDING_TIME: 5 + CONF_PENDING_TIME: datetime.timedelta(seconds=5), }, STATE_ALARM_ARMED_CUSTOM_BYPASS: { - CONF_PENDING_TIME: 5 + CONF_PENDING_TIME: datetime.timedelta(seconds=5), }, STATE_ALARM_TRIGGERED: { - CONF_PENDING_TIME: 5 + CONF_PENDING_TIME: datetime.timedelta(seconds=5), }, }), ]) diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index e126074e60744..257c3a8a2066b 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import track_point_in_time DEFAULT_ALARM_NAME = 'HA Alarm' -DEFAULT_PENDING_TIME = 60 +DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60) DEFAULT_TRIGGER_TIME = 120 DEFAULT_DISARM_AFTER_TRIGGER = False @@ -44,7 +44,7 @@ def _state_validator(config): STATE_SETTING_SCHEMA = vol.Schema({ vol.Optional(CONF_PENDING_TIME): - vol.All(vol.Coerce(int), vol.Range(min=0)) + vol.All(cv.time_period, cv.positive_timedelta), }) @@ -53,7 +53,7 @@ def _state_validator(config): vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): - vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Optional(CONF_DISARM_AFTER_TRIGGER, @@ -75,7 +75,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, config[CONF_NAME], config.get(CONF_CODE), - config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME), config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME), config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER), config @@ -92,7 +91,7 @@ class ManualAlarm(alarm.AlarmControlPanel): the previous state or disarm if `disarm_after_trigger` is true. """ - def __init__(self, hass, name, code, pending_time, trigger_time, + def __init__(self, hass, name, code, trigger_time, disarm_after_trigger, config): """Init the manual alarm panel.""" self._state = STATE_ALARM_DISARMED @@ -104,10 +103,9 @@ def __init__(self, hass, name, code, pending_time, trigger_time, self._previous_state = self._state self._state_ts = None - self._pending_time_by_state = {} - for state in SUPPORTED_PENDING_STATES: - self._pending_time_by_state[state] = datetime.timedelta( - seconds=config[state][CONF_PENDING_TIME]) + self._pending_time_by_state = { + state: config[state][CONF_PENDING_TIME] + for state in SUPPORTED_PENDING_STATES} @property def should_poll(self): diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 965af9bcd0752..3d96ae173d61e 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -32,7 +32,7 @@ CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night' DEFAULT_ALARM_NAME = 'HA Alarm' -DEFAULT_PENDING_TIME = 60 +DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60) DEFAULT_TRIGGER_TIME = 120 DEFAULT_DISARM_AFTER_TRIGGER = False DEFAULT_ARM_AWAY = 'ARM_AWAY' @@ -58,7 +58,7 @@ def _state_validator(config): STATE_SETTING_SCHEMA = vol.Schema({ vol.Optional(CONF_PENDING_TIME): - vol.All(vol.Coerce(int), vol.Range(min=0)) + vol.All(cv.time_period, cv.positive_timedelta), }) DEPENDENCIES = ['mqtt'] @@ -68,7 +68,7 @@ def _state_validator(config): vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): - vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Optional(CONF_DISARM_AFTER_TRIGGER, @@ -94,7 +94,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, config[CONF_NAME], config.get(CONF_CODE), - config.get(CONF_PENDING_TIME, DEFAULT_PENDING_TIME), config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME), config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER), config.get(mqtt.CONF_STATE_TOPIC), @@ -117,7 +116,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): the previous state or disarm if `disarm_after_trigger` is true. """ - def __init__(self, hass, name, code, pending_time, + def __init__(self, hass, name, code, trigger_time, disarm_after_trigger, state_topic, command_topic, qos, payload_disarm, payload_arm_home, payload_arm_away, @@ -127,16 +126,14 @@ def __init__(self, hass, name, code, pending_time, self._hass = hass self._name = name self._code = str(code) if code else None - self._pending_time = datetime.timedelta(seconds=pending_time) self._trigger_time = datetime.timedelta(seconds=trigger_time) self._disarm_after_trigger = disarm_after_trigger self._previous_state = self._state self._state_ts = None - self._pending_time_by_state = {} - for state in SUPPORTED_PENDING_STATES: - self._pending_time_by_state[state] = datetime.timedelta( - seconds=config[state][CONF_PENDING_TIME]) + self._pending_time_by_state = { + state: config[state][CONF_PENDING_TIME] + for state in SUPPORTED_PENDING_STATES} self._state_topic = state_topic self._command_topic = command_topic From 401caab129a4adab4cfecdff018c197eef991663 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Mon, 27 Nov 2017 09:58:56 +0100 Subject: [PATCH 5/8] alarm/manual, alarm/manual_mqtt: cleanup trigger_time Change it to cv.time_period; in order to use cv.positive_timedelta, fix up the code to allow specifying a zero trigger time. A zero trigger time disables triggering the alarm. The code is more or less already there, but was not active because the schema requests a minimum of one second for the trigger_time. This is not very useful as is, but it will be after the next patch to make the trigger time customizable per-state. --- .../components/alarm_control_panel/demo.py | 3 +- .../components/alarm_control_panel/manual.py | 18 +++++-- .../alarm_control_panel/manual_mqtt.py | 18 +++++-- .../alarm_control_panel/test_manual.py | 46 +++++++++++++++++ .../alarm_control_panel/test_manual_mqtt.py | 50 +++++++++++++++++++ 5 files changed, 124 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/demo.py b/homeassistant/components/alarm_control_panel/demo.py index ba7419dffedc4..d33635ef15124 100644 --- a/homeassistant/components/alarm_control_panel/demo.py +++ b/homeassistant/components/alarm_control_panel/demo.py @@ -14,7 +14,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo alarm control panel platform.""" add_devices([ - manual.ManualAlarm(hass, 'Alarm', '1234', 10, False, { + manual.ManualAlarm(hass, 'Alarm', '1234', + datetime.timedelta(seconds=10), False, { STATE_ALARM_ARMED_AWAY: { CONF_PENDING_TIME: datetime.timedelta(seconds=5), }, diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 257c3a8a2066b..17b8dc864ee21 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -22,7 +22,7 @@ DEFAULT_ALARM_NAME = 'HA Alarm' DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60) -DEFAULT_TRIGGER_TIME = 120 +DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120) DEFAULT_DISARM_AFTER_TRIGGER = False SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -55,7 +55,7 @@ def _state_validator(config): vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): - vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean, vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA, @@ -89,6 +89,7 @@ class ManualAlarm(alarm.AlarmControlPanel): When triggered, will be pending for the triggered state's 'pending_time'. After that will be triggered for 'trigger_time', after that we return to the previous state or disarm if `disarm_after_trigger` is true. + A trigger_time of zero disables the alarm_trigger service. """ def __init__(self, hass, name, code, trigger_time, @@ -98,7 +99,7 @@ def __init__(self, hass, name, code, trigger_time, self._hass = hass self._name = name self._code = str(code) if code else None - self._trigger_time = datetime.timedelta(seconds=trigger_time) + self._trigger_time = trigger_time self._disarm_after_trigger = disarm_after_trigger self._previous_state = self._state self._state_ts = None @@ -120,7 +121,7 @@ def name(self): @property def state(self): """Return the state of the device.""" - if self._state == STATE_ALARM_TRIGGERED and self._trigger_time: + if self._state == STATE_ALARM_TRIGGERED: if self._within_pending_time(self._state): return STATE_ALARM_PENDING elif (self._state_ts + self._pending_time_by_state[self._state] + @@ -184,7 +185,14 @@ def alarm_arm_custom_bypass(self, code=None): self._update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS) def alarm_trigger(self, code=None): - """Send alarm trigger command. No code needed.""" + """ + Send alarm trigger command. + + No code needed, a trigger time of zero disables the alarm. + """ + if not self._trigger_time: + return + self._update_state(STATE_ALARM_TRIGGERED) def _update_state(self, state): diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 3d96ae173d61e..ef1c5e487f10a 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -33,7 +33,7 @@ DEFAULT_ALARM_NAME = 'HA Alarm' DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60) -DEFAULT_TRIGGER_TIME = 120 +DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120) DEFAULT_DISARM_AFTER_TRIGGER = False DEFAULT_ARM_AWAY = 'ARM_AWAY' DEFAULT_ARM_HOME = 'ARM_HOME' @@ -70,7 +70,7 @@ def _state_validator(config): vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): - vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean, vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA, @@ -114,6 +114,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): When triggered, will be pending for the triggered state's 'pending_time'. After that will be triggered for 'trigger_time', after that we return to the previous state or disarm if `disarm_after_trigger` is true. + A trigger_time of zero disables the alarm_trigger service. """ def __init__(self, hass, name, code, @@ -126,7 +127,7 @@ def __init__(self, hass, name, code, self._hass = hass self._name = name self._code = str(code) if code else None - self._trigger_time = datetime.timedelta(seconds=trigger_time) + self._trigger_time = trigger_time self._disarm_after_trigger = disarm_after_trigger self._previous_state = self._state self._state_ts = None @@ -156,7 +157,7 @@ def name(self): @property def state(self): """Return the state of the device.""" - if self._state == STATE_ALARM_TRIGGERED and self._trigger_time: + if self._state == STATE_ALARM_TRIGGERED: if self._within_pending_time(self._state): return STATE_ALARM_PENDING elif (self._state_ts + self._pending_time_by_state[self._state] + @@ -213,7 +214,14 @@ def alarm_arm_night(self, code=None): self._update_state(STATE_ALARM_ARMED_NIGHT) def alarm_trigger(self, code=None): - """Send alarm trigger command. No code needed.""" + """ + Send alarm trigger command. + + No code needed, a trigger time of zero disables the alarm. + """ + if not self._trigger_time: + return + self._update_state(STATE_ALARM_TRIGGERED) def _update_state(self, state): diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py index 9cc5e673cd79d..d206e3a59c9fb 100644 --- a/tests/components/alarm_control_panel/test_manual.py +++ b/tests/components/alarm_control_panel/test_manual.py @@ -318,6 +318,52 @@ def test_trigger_no_pending(self): self.assertEqual(STATE_ALARM_TRIGGERED, self.hass.states.get(entity_id).state) + def test_trigger_zero_trigger_time(self): + """Test disabled trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'pending_time': 0, + 'trigger_time': 0, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_zero_trigger_time_with_pending(self): + """Test disabled trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'pending_time': 2, + 'trigger_time': 0, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + def test_trigger_with_pending(self): """Test arm home method.""" self.assertTrue(setup_component( diff --git a/tests/components/alarm_control_panel/test_manual_mqtt.py b/tests/components/alarm_control_panel/test_manual_mqtt.py index 0d79e85287788..c21b0f4edfa43 100644 --- a/tests/components/alarm_control_panel/test_manual_mqtt.py +++ b/tests/components/alarm_control_panel/test_manual_mqtt.py @@ -352,6 +352,56 @@ def test_trigger_no_pending(self): self.assertEqual(STATE_ALARM_TRIGGERED, self.hass.states.get(entity_id).state) + def test_trigger_zero_trigger_time(self): + """Test disabled trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'pending_time': 0, + 'trigger_time': 0, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_zero_trigger_time_with_pending(self): + """Test disabled trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'pending_time': 2, + 'trigger_time': 0, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + def test_trigger_with_pending(self): """Test arm home method.""" self.assertTrue(setup_component( From 7f8c557026a93b13ce6b8aa210ef77fb2700850c Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Mon, 27 Nov 2017 10:01:19 +0100 Subject: [PATCH 6/8] alarm/manual, alarm/manual_mqtt: add per-state trigger_time During a pending state, the trigger_time is the one of the previous state. This lets the user implement a "true" disarmed state, without having to test it in the conditions for the alarm control panel's state. This simplifies the automation if you are not interested in multiple states. For example you can just add disarmed: trigger_time: 0 in the manual alarm and omit the condition: - condition: state entity_id: alarm_control_panel.ha_alarm state: armed_away in all the actions. Signed-off-by: Paolo Bonzini --- .../components/alarm_control_panel/demo.py | 16 ++- .../components/alarm_control_panel/manual.py | 76 +++++++++---- .../alarm_control_panel/manual_mqtt.py | 72 +++++++++---- .../alarm_control_panel/test_manual.py | 95 ++++++++++++++++ .../alarm_control_panel/test_manual_mqtt.py | 101 ++++++++++++++++++ 5 files changed, 313 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/demo.py b/homeassistant/components/alarm_control_panel/demo.py index d33635ef15124..9dc55e8e2d460 100644 --- a/homeassistant/components/alarm_control_panel/demo.py +++ b/homeassistant/components/alarm_control_panel/demo.py @@ -7,26 +7,34 @@ import datetime import homeassistant.components.alarm_control_panel.manual as manual from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED, CONF_PENDING_TIME) + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, + CONF_PENDING_TIME, CONF_TRIGGER_TIME) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo alarm control panel platform.""" add_devices([ - manual.ManualAlarm(hass, 'Alarm', '1234', - datetime.timedelta(seconds=10), False, { + manual.ManualAlarm(hass, 'Alarm', '1234', False, { STATE_ALARM_ARMED_AWAY: { CONF_PENDING_TIME: datetime.timedelta(seconds=5), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_ARMED_HOME: { CONF_PENDING_TIME: datetime.timedelta(seconds=5), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_ARMED_NIGHT: { CONF_PENDING_TIME: datetime.timedelta(seconds=5), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), + }, + STATE_ALARM_DISARMED: { + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_ARMED_CUSTOM_BYPASS: { CONF_PENDING_TIME: datetime.timedelta(seconds=5), + CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_TRIGGERED: { CONF_PENDING_TIME: datetime.timedelta(seconds=5), diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 17b8dc864ee21..04e87b559c6d6 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -25,9 +25,15 @@ DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120) DEFAULT_DISARM_AFTER_TRIGGER = False -SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, - STATE_ALARM_ARMED_CUSTOM_BYPASS] +SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED] + +SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES + if state != STATE_ALARM_TRIGGERED] + +SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES + if state != STATE_ALARM_DISARMED] ATTR_PRE_PENDING_STATE = 'pre_pending_state' ATTR_POST_PENDING_STATE = 'post_pending_state' @@ -35,6 +41,9 @@ def _state_validator(config): config = copy.deepcopy(config) + for state in SUPPORTED_PRETRIGGER_STATES: + if CONF_TRIGGER_TIME not in config[state]: + config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME] for state in SUPPORTED_PENDING_STATES: if CONF_PENDING_TIME not in config[state]: config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME] @@ -42,10 +51,15 @@ def _state_validator(config): return config -STATE_SETTING_SCHEMA = vol.Schema({ - vol.Optional(CONF_PENDING_TIME): - vol.All(cv.time_period, cv.positive_timedelta), -}) +def _state_schema(state): + schema = {} + if state in SUPPORTED_PRETRIGGER_STATES: + schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All( + cv.time_period, cv.positive_timedelta) + if state in SUPPORTED_PENDING_STATES: + schema[vol.Optional(CONF_PENDING_TIME)] = vol.All( + cv.time_period, cv.positive_timedelta) + return vol.Schema(schema) PLATFORM_SCHEMA = vol.Schema(vol.All({ @@ -58,12 +72,18 @@ def _state_validator(config): vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean, - vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, - default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA, + vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): + _state_schema(STATE_ALARM_ARMED_AWAY), + vol.Optional(STATE_ALARM_ARMED_HOME, default={}): + _state_schema(STATE_ALARM_ARMED_HOME), + vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): + _state_schema(STATE_ALARM_ARMED_NIGHT), + vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}): + _state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS), + vol.Optional(STATE_ALARM_DISARMED, default={}): + _state_schema(STATE_ALARM_DISARMED), + vol.Optional(STATE_ALARM_TRIGGERED, default={}): + _state_schema(STATE_ALARM_TRIGGERED), }, _state_validator)) _LOGGER = logging.getLogger(__name__) @@ -75,7 +95,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, config[CONF_NAME], config.get(CONF_CODE), - config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME), config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER), config )]) @@ -92,18 +111,20 @@ class ManualAlarm(alarm.AlarmControlPanel): A trigger_time of zero disables the alarm_trigger service. """ - def __init__(self, hass, name, code, trigger_time, + def __init__(self, hass, name, code, disarm_after_trigger, config): """Init the manual alarm panel.""" self._state = STATE_ALARM_DISARMED self._hass = hass self._name = name self._code = str(code) if code else None - self._trigger_time = trigger_time self._disarm_after_trigger = disarm_after_trigger self._previous_state = self._state self._state_ts = None + self._trigger_time_by_state = { + state: config[state][CONF_TRIGGER_TIME] + for state in SUPPORTED_PRETRIGGER_STATES} self._pending_time_by_state = { state: config[state][CONF_PENDING_TIME] for state in SUPPORTED_PENDING_STATES} @@ -124,8 +145,9 @@ def state(self): if self._state == STATE_ALARM_TRIGGERED: if self._within_pending_time(self._state): return STATE_ALARM_PENDING - elif (self._state_ts + self._pending_time_by_state[self._state] + - self._trigger_time) < dt_util.utcnow(): + trigger_time = self._trigger_time_by_state[self._previous_state] + if (self._state_ts + self._pending_time_by_state[self._state] + + trigger_time) < dt_util.utcnow(): if self._disarm_after_trigger: return STATE_ALARM_DISARMED else: @@ -138,6 +160,13 @@ def state(self): return self._state + @property + def _active_state(self): + if self.state == STATE_ALARM_PENDING: + return self._previous_state + else: + return self._state + def _within_pending_time(self, state): pending_time = self._pending_time_by_state[state] return self._state_ts + pending_time > dt_util.utcnow() @@ -188,11 +217,11 @@ def alarm_trigger(self, code=None): """ Send alarm trigger command. - No code needed, a trigger time of zero disables the alarm. + No code needed, a trigger time of zero for the current state + disables the alarm. """ - if not self._trigger_time: + if not self._trigger_time_by_state[self._active_state]: return - self._update_state(STATE_ALARM_TRIGGERED) def _update_state(self, state): @@ -206,14 +235,15 @@ def _update_state(self, state): pending_time = self._pending_time_by_state[state] - if state == STATE_ALARM_TRIGGERED and self._trigger_time: + if state == STATE_ALARM_TRIGGERED: track_point_in_time( self._hass, self.async_update_ha_state, self._state_ts + pending_time) + trigger_time = self._trigger_time_by_state[self._previous_state] track_point_in_time( self._hass, self.async_update_ha_state, - self._state_ts + self._trigger_time + pending_time) + self._state_ts + pending_time + trigger_time) elif state in SUPPORTED_PENDING_STATES and pending_time: track_point_in_time( self._hass, self.async_update_ha_state, diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index ef1c5e487f10a..6474ac6de3d89 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -40,8 +40,15 @@ DEFAULT_ARM_NIGHT = 'ARM_NIGHT' DEFAULT_DISARM = 'DISARM' -SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED] +SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_TRIGGERED] + +SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES + if state != STATE_ALARM_TRIGGERED] + +SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES + if state != STATE_ALARM_DISARMED] ATTR_PRE_PENDING_STATE = 'pre_pending_state' ATTR_POST_PENDING_STATE = 'post_pending_state' @@ -49,6 +56,9 @@ def _state_validator(config): config = copy.deepcopy(config) + for state in SUPPORTED_PRETRIGGER_STATES: + if CONF_TRIGGER_TIME not in config[state]: + config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME] for state in SUPPORTED_PENDING_STATES: if CONF_PENDING_TIME not in config[state]: config[state][CONF_PENDING_TIME] = config[CONF_PENDING_TIME] @@ -56,10 +66,16 @@ def _state_validator(config): return config -STATE_SETTING_SCHEMA = vol.Schema({ - vol.Optional(CONF_PENDING_TIME): - vol.All(cv.time_period, cv.positive_timedelta), -}) +def _state_schema(state): + schema = {} + if state in SUPPORTED_PRETRIGGER_STATES: + schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All( + cv.time_period, cv.positive_timedelta) + if state in SUPPORTED_PENDING_STATES: + schema[vol.Optional(CONF_PENDING_TIME)] = vol.All( + cv.time_period, cv.positive_timedelta) + return vol.Schema(schema) + DEPENDENCIES = ['mqtt'] @@ -73,10 +89,16 @@ def _state_validator(config): vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean, - vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA, - vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA, + vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): + _state_schema(STATE_ALARM_ARMED_AWAY), + vol.Optional(STATE_ALARM_ARMED_HOME, default={}): + _state_schema(STATE_ALARM_ARMED_HOME), + vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): + _state_schema(STATE_ALARM_ARMED_NIGHT), + vol.Optional(STATE_ALARM_DISARMED, default={}): + _state_schema(STATE_ALARM_DISARMED), + vol.Optional(STATE_ALARM_TRIGGERED, default={}): + _state_schema(STATE_ALARM_TRIGGERED), vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, @@ -94,7 +116,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, config[CONF_NAME], config.get(CONF_CODE), - config.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME), config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER), config.get(mqtt.CONF_STATE_TOPIC), config.get(mqtt.CONF_COMMAND_TOPIC), @@ -118,7 +139,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): """ def __init__(self, hass, name, code, - trigger_time, disarm_after_trigger, + disarm_after_trigger, state_topic, command_topic, qos, payload_disarm, payload_arm_home, payload_arm_away, payload_arm_night, config): @@ -127,11 +148,13 @@ def __init__(self, hass, name, code, self._hass = hass self._name = name self._code = str(code) if code else None - self._trigger_time = trigger_time self._disarm_after_trigger = disarm_after_trigger self._previous_state = self._state self._state_ts = None + self._trigger_time_by_state = { + state: config[state][CONF_TRIGGER_TIME] + for state in SUPPORTED_PRETRIGGER_STATES} self._pending_time_by_state = { state: config[state][CONF_PENDING_TIME] for state in SUPPORTED_PENDING_STATES} @@ -160,8 +183,9 @@ def state(self): if self._state == STATE_ALARM_TRIGGERED: if self._within_pending_time(self._state): return STATE_ALARM_PENDING - elif (self._state_ts + self._pending_time_by_state[self._state] + - self._trigger_time) < dt_util.utcnow(): + trigger_time = self._trigger_time_by_state[self._previous_state] + if (self._state_ts + self._pending_time_by_state[self._state] + + trigger_time) < dt_util.utcnow(): if self._disarm_after_trigger: return STATE_ALARM_DISARMED else: @@ -174,6 +198,13 @@ def state(self): return self._state + @property + def _active_state(self): + if self.state == STATE_ALARM_PENDING: + return self._previous_state + else: + return self._state + def _within_pending_time(self, state): pending_time = self._pending_time_by_state[state] return self._state_ts + pending_time > dt_util.utcnow() @@ -217,11 +248,11 @@ def alarm_trigger(self, code=None): """ Send alarm trigger command. - No code needed, a trigger time of zero disables the alarm. + No code needed, a trigger time of zero for the current state + disables the alarm. """ - if not self._trigger_time: + if not self._trigger_time_by_state[self._active_state]: return - self._update_state(STATE_ALARM_TRIGGERED) def _update_state(self, state): @@ -235,14 +266,15 @@ def _update_state(self, state): pending_time = self._pending_time_by_state[state] - if state == STATE_ALARM_TRIGGERED and self._trigger_time: + if state == STATE_ALARM_TRIGGERED: track_point_in_time( self._hass, self.async_update_ha_state, self._state_ts + pending_time) + trigger_time = self._trigger_time_by_state[self._previous_state] track_point_in_time( self._hass, self.async_update_ha_state, - self._state_ts + self._trigger_time + pending_time) + self._state_ts + pending_time + trigger_time) elif state in SUPPORTED_PENDING_STATES and pending_time: track_point_in_time( self._hass, self.async_update_ha_state, diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py index d206e3a59c9fb..970667b50248a 100644 --- a/tests/components/alarm_control_panel/test_manual.py +++ b/tests/components/alarm_control_panel/test_manual.py @@ -571,6 +571,101 @@ def test_trigger_with_disarm_after_trigger(self): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) + def test_trigger_with_zero_specific_trigger_time(self): + """Test trigger method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'trigger_time': 5, + 'disarmed': { + 'trigger_time': 0 + }, + 'pending_time': 0, + 'disarm_after_trigger': True + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_with_unused_zero_specific_trigger_time(self): + """Test disarm after trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'trigger_time': 5, + 'armed_home': { + 'trigger_time': 0 + }, + 'pending_time': 0, + 'disarm_after_trigger': True + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_with_specific_trigger_time(self): + """Test disarm after trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'disarmed': { + 'trigger_time': 5 + }, + 'pending_time': 0, + 'disarm_after_trigger': True + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + def test_trigger_with_no_disarm_after_trigger(self): """Test disarm after trigger.""" self.assertTrue(setup_component( diff --git a/tests/components/alarm_control_panel/test_manual_mqtt.py b/tests/components/alarm_control_panel/test_manual_mqtt.py index c21b0f4edfa43..a31d5f1bb39ea 100644 --- a/tests/components/alarm_control_panel/test_manual_mqtt.py +++ b/tests/components/alarm_control_panel/test_manual_mqtt.py @@ -482,6 +482,107 @@ def test_trigger_with_disarm_after_trigger(self): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) + def test_trigger_with_zero_specific_trigger_time(self): + """Test trigger method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'trigger_time': 5, + 'disarmed': { + 'trigger_time': 0 + }, + 'pending_time': 0, + 'disarm_after_trigger': True, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_with_unused_zero_specific_trigger_time(self): + """Test disarm after trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'trigger_time': 5, + 'armed_home': { + 'trigger_time': 0 + }, + 'pending_time': 0, + 'disarm_after_trigger': True, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_with_specific_trigger_time(self): + """Test disarm after trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'disarmed': { + 'trigger_time': 5 + }, + 'pending_time': 0, + 'disarm_after_trigger': True, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + def test_back_to_back_trigger_with_no_disarm_after_trigger(self): """Test no disarm after back to back trigger.""" self.assertTrue(setup_component( From 34749112832550e48988c44f32e66dd123c01a37 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Tue, 21 Nov 2017 23:48:30 +0100 Subject: [PATCH 7/8] alarm/manual, alarm/manual_mqtt: add delay_time setting The delay_time argument is complementary to pending_time. Instead of causing the alarm to become PENDING when switching to a state, it causes the alarm to become PENDING when switching *from* a state to the TRIGGERED state. For example, you could set a delay_time to 30 if opening the garage door will take 30 seconds, during which the dog could walk in front of the door and trigger a presence alarm. The delay time can also be set per state; for example, at night doors are supposed to be closed, and a presence alarm would necessarily be triggered "by the inside"; so "armed_night" would have zero delay_time. Signed-off-by: Paolo Bonzini --- .../components/alarm_control_panel/demo.py | 7 +- .../components/alarm_control_panel/manual.py | 30 +- .../alarm_control_panel/manual_mqtt.py | 31 +- homeassistant/const.py | 1 + .../alarm_control_panel/test_manual.py | 310 +++++++++++++++++ .../alarm_control_panel/test_manual_mqtt.py | 320 ++++++++++++++++++ 6 files changed, 683 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/demo.py b/homeassistant/components/alarm_control_panel/demo.py index 9dc55e8e2d460..6365c89450297 100644 --- a/homeassistant/components/alarm_control_panel/demo.py +++ b/homeassistant/components/alarm_control_panel/demo.py @@ -9,7 +9,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, + STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, CONF_DELAY_TIME, CONF_PENDING_TIME, CONF_TRIGGER_TIME) @@ -18,21 +18,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([ manual.ManualAlarm(hass, 'Alarm', '1234', False, { STATE_ALARM_ARMED_AWAY: { + CONF_DELAY_TIME: datetime.timedelta(seconds=0), CONF_PENDING_TIME: datetime.timedelta(seconds=5), CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_ARMED_HOME: { + CONF_DELAY_TIME: datetime.timedelta(seconds=0), CONF_PENDING_TIME: datetime.timedelta(seconds=5), CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_ARMED_NIGHT: { + CONF_DELAY_TIME: datetime.timedelta(seconds=0), CONF_PENDING_TIME: datetime.timedelta(seconds=5), CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_DISARMED: { + CONF_DELAY_TIME: datetime.timedelta(seconds=0), CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, STATE_ALARM_ARMED_CUSTOM_BYPASS: { + CONF_DELAY_TIME: datetime.timedelta(seconds=0), CONF_PENDING_TIME: datetime.timedelta(seconds=5), CONF_TRIGGER_TIME: datetime.timedelta(seconds=10), }, diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 04e87b559c6d6..fea9bd401721d 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -16,11 +16,13 @@ STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME, CONF_CODE, - CONF_PENDING_TIME, CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER) + CONF_DELAY_TIME, CONF_PENDING_TIME, CONF_TRIGGER_TIME, + CONF_DISARM_AFTER_TRIGGER) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time DEFAULT_ALARM_NAME = 'HA Alarm' +DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0) DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60) DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120) DEFAULT_DISARM_AFTER_TRIGGER = False @@ -42,6 +44,8 @@ def _state_validator(config): config = copy.deepcopy(config) for state in SUPPORTED_PRETRIGGER_STATES: + if CONF_DELAY_TIME not in config[state]: + config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME] if CONF_TRIGGER_TIME not in config[state]: config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME] for state in SUPPORTED_PENDING_STATES: @@ -54,6 +58,8 @@ def _state_validator(config): def _state_schema(state): schema = {} if state in SUPPORTED_PRETRIGGER_STATES: + schema[vol.Optional(CONF_DELAY_TIME)] = vol.All( + cv.time_period, cv.positive_timedelta) schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All( cv.time_period, cv.positive_timedelta) if state in SUPPORTED_PENDING_STATES: @@ -66,6 +72,8 @@ def _state_schema(state): vol.Required(CONF_PLATFORM): 'manual', vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, vol.Optional(CONF_CODE): cv.string, + vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): + vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): @@ -105,7 +113,8 @@ class ManualAlarm(alarm.AlarmControlPanel): Representation of an alarm status. When armed, will be pending for 'pending_time', after that armed. - When triggered, will be pending for the triggered state's 'pending_time'. + When triggered, will be pending for the triggering state's 'delay_time' + plus the triggered state's 'pending_time'. After that will be triggered for 'trigger_time', after that we return to the previous state or disarm if `disarm_after_trigger` is true. A trigger_time of zero disables the alarm_trigger service. @@ -122,6 +131,9 @@ def __init__(self, hass, name, code, self._previous_state = self._state self._state_ts = None + self._delay_time_by_state = { + state: config[state][CONF_DELAY_TIME] + for state in SUPPORTED_PRETRIGGER_STATES} self._trigger_time_by_state = { state: config[state][CONF_TRIGGER_TIME] for state in SUPPORTED_PRETRIGGER_STATES} @@ -146,7 +158,7 @@ def state(self): if self._within_pending_time(self._state): return STATE_ALARM_PENDING trigger_time = self._trigger_time_by_state[self._previous_state] - if (self._state_ts + self._pending_time_by_state[self._state] + + if (self._state_ts + self._pending_time(self._state) + trigger_time) < dt_util.utcnow(): if self._disarm_after_trigger: return STATE_ALARM_DISARMED @@ -167,9 +179,14 @@ def _active_state(self): else: return self._state - def _within_pending_time(self, state): + def _pending_time(self, state): pending_time = self._pending_time_by_state[state] - return self._state_ts + pending_time > dt_util.utcnow() + if state == STATE_ALARM_TRIGGERED: + pending_time += self._delay_time_by_state[self._previous_state] + return pending_time + + def _within_pending_time(self, state): + return self._state_ts + self._pending_time(state) > dt_util.utcnow() @property def code_format(self): @@ -233,8 +250,7 @@ def _update_state(self, state): self._state_ts = dt_util.utcnow() self.schedule_update_ha_state() - pending_time = self._pending_time_by_state[state] - + pending_time = self._pending_time(state) if state == STATE_ALARM_TRIGGERED: track_point_in_time( self._hass, self.async_update_ha_state, diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 6474ac6de3d89..6713a77be8d04 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -16,8 +16,8 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, - CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME, - CONF_DISARM_AFTER_TRIGGER) + CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME, + CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER) import homeassistant.components.mqtt as mqtt from homeassistant.helpers.event import async_track_state_change @@ -32,6 +32,7 @@ CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night' DEFAULT_ALARM_NAME = 'HA Alarm' +DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0) DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60) DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120) DEFAULT_DISARM_AFTER_TRIGGER = False @@ -57,6 +58,8 @@ def _state_validator(config): config = copy.deepcopy(config) for state in SUPPORTED_PRETRIGGER_STATES: + if CONF_DELAY_TIME not in config[state]: + config[state][CONF_DELAY_TIME] = config[CONF_DELAY_TIME] if CONF_TRIGGER_TIME not in config[state]: config[state][CONF_TRIGGER_TIME] = config[CONF_TRIGGER_TIME] for state in SUPPORTED_PENDING_STATES: @@ -69,6 +72,8 @@ def _state_validator(config): def _state_schema(state): schema = {} if state in SUPPORTED_PRETRIGGER_STATES: + schema[vol.Optional(CONF_DELAY_TIME)] = vol.All( + cv.time_period, cv.positive_timedelta) schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All( cv.time_period, cv.positive_timedelta) if state in SUPPORTED_PENDING_STATES: @@ -83,6 +88,8 @@ def _state_schema(state): vol.Required(CONF_PLATFORM): 'manual_mqtt', vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, vol.Optional(CONF_CODE): cv.string, + vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): + vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): @@ -132,7 +139,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): Representation of an alarm status. When armed, will be pending for 'pending_time', after that armed. - When triggered, will be pending for the triggered state's 'pending_time'. + When triggered, will be pending for the triggering state's 'delay_time' + plus the triggered state's 'pending_time'. After that will be triggered for 'trigger_time', after that we return to the previous state or disarm if `disarm_after_trigger` is true. A trigger_time of zero disables the alarm_trigger service. @@ -152,6 +160,9 @@ def __init__(self, hass, name, code, self._previous_state = self._state self._state_ts = None + self._delay_time_by_state = { + state: config[state][CONF_DELAY_TIME] + for state in SUPPORTED_PRETRIGGER_STATES} self._trigger_time_by_state = { state: config[state][CONF_TRIGGER_TIME] for state in SUPPORTED_PRETRIGGER_STATES} @@ -184,7 +195,7 @@ def state(self): if self._within_pending_time(self._state): return STATE_ALARM_PENDING trigger_time = self._trigger_time_by_state[self._previous_state] - if (self._state_ts + self._pending_time_by_state[self._state] + + if (self._state_ts + self._pending_time(self._state) + trigger_time) < dt_util.utcnow(): if self._disarm_after_trigger: return STATE_ALARM_DISARMED @@ -205,9 +216,14 @@ def _active_state(self): else: return self._state - def _within_pending_time(self, state): + def _pending_time(self, state): pending_time = self._pending_time_by_state[state] - return self._state_ts + pending_time > dt_util.utcnow() + if state == STATE_ALARM_TRIGGERED: + pending_time += self._delay_time_by_state[self._previous_state] + return pending_time + + def _within_pending_time(self, state): + return self._state_ts + self._pending_time(state) > dt_util.utcnow() @property def code_format(self): @@ -264,8 +280,7 @@ def _update_state(self, state): self._state_ts = dt_util.utcnow() self.schedule_update_ha_state() - pending_time = self._pending_time_by_state[state] - + pending_time = self._pending_time(state) if state == STATE_ALARM_TRIGGERED: track_point_in_time( self._hass, self.async_update_ha_state, diff --git a/homeassistant/const.py b/homeassistant/const.py index f46058b186c15..c9615128efd7d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -52,6 +52,7 @@ CONF_CUSTOMIZE = 'customize' CONF_CUSTOMIZE_DOMAIN = 'customize_domain' CONF_CUSTOMIZE_GLOB = 'customize_glob' +CONF_DELAY_TIME = 'delay_time' CONF_DEVICE = 'device' CONF_DEVICE_CLASS = 'device_class' CONF_DEVICES = 'devices' diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py index 970667b50248a..ade55642677bf 100644 --- a/tests/components/alarm_control_panel/test_manual.py +++ b/tests/components/alarm_control_panel/test_manual.py @@ -318,6 +318,47 @@ def test_trigger_no_pending(self): self.assertEqual(STATE_ALARM_TRIGGERED, self.hass.states.get(entity_id).state) + def test_trigger_with_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'delay_time': 1, + 'pending_time': 0, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_TRIGGERED, state.state) + def test_trigger_zero_trigger_time(self): """Test disabled trigger.""" self.assertTrue(setup_component( @@ -408,6 +449,203 @@ def test_trigger_with_pending(self): state = self.hass.states.get(entity_id) assert state.state == STATE_ALARM_DISARMED + def test_trigger_with_unused_specific_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'delay_time': 5, + 'pending_time': 0, + 'armed_home': { + 'delay_time': 10 + }, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + + def test_trigger_with_specific_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'delay_time': 10, + 'pending_time': 0, + 'armed_away': { + 'delay_time': 1 + }, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + + def test_trigger_with_pending_and_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'delay_time': 1, + 'pending_time': 0, + 'triggered': { + 'pending_time': 1 + }, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future += timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + + def test_trigger_with_pending_and_specific_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'delay_time': 10, + 'pending_time': 0, + 'armed_away': { + 'delay_time': 1 + }, + 'triggered': { + 'pending_time': 1 + }, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future += timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + def test_armed_home_with_specific_pending(self): """Test arm home method.""" self.assertTrue(setup_component( @@ -943,3 +1181,75 @@ def test_armed_custom_bypass_with_specific_pending(self): self.assertEqual(STATE_ALARM_ARMED_CUSTOM_BYPASS, self.hass.states.get(entity_id).state) + + def test_arm_away_after_disabled_disarmed(self): + """Test pending state with and without zero trigger time.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'pending_time': 0, + 'delay_time': 1, + 'armed_away': { + 'pending_time': 1, + }, + 'disarmed': { + 'trigger_time': 0 + }, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_DISARMED, + state.attributes['pre_pending_state']) + self.assertEqual(STATE_ALARM_ARMED_AWAY, + state.attributes['post_pending_state']) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_DISARMED, + state.attributes['pre_pending_state']) + self.assertEqual(STATE_ALARM_ARMED_AWAY, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_AWAY, state.state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_ARMED_AWAY, + state.attributes['pre_pending_state']) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future += timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_TRIGGERED, state.state) diff --git a/tests/components/alarm_control_panel/test_manual_mqtt.py b/tests/components/alarm_control_panel/test_manual_mqtt.py index a31d5f1bb39ea..7d7eb7b8175c5 100644 --- a/tests/components/alarm_control_panel/test_manual_mqtt.py +++ b/tests/components/alarm_control_panel/test_manual_mqtt.py @@ -352,6 +352,49 @@ def test_trigger_no_pending(self): self.assertEqual(STATE_ALARM_TRIGGERED, self.hass.states.get(entity_id).state) + def test_trigger_with_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'delay_time': 1, + 'pending_time': 0, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_TRIGGERED, state.state) + def test_trigger_zero_trigger_time(self): """Test disabled trigger.""" self.assertTrue(setup_component( @@ -717,6 +760,211 @@ def test_disarm_during_trigger_with_invalid_code(self): self.assertEqual(STATE_ALARM_TRIGGERED, self.hass.states.get(entity_id).state) + def test_trigger_with_unused_specific_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'delay_time': 5, + 'pending_time': 0, + 'armed_home': { + 'delay_time': 10 + }, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + + def test_trigger_with_specific_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'delay_time': 10, + 'pending_time': 0, + 'armed_away': { + 'delay_time': 1 + }, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + + def test_trigger_with_pending_and_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'delay_time': 1, + 'pending_time': 0, + 'triggered': { + 'pending_time': 1 + }, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future += timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + + def test_trigger_with_pending_and_specific_delay(self): + """Test trigger method and switch from pending to triggered.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'delay_time': 10, + 'pending_time': 0, + 'armed_away': { + 'delay_time': 1 + }, + 'triggered': { + 'pending_time': 1 + }, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state' + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_PENDING + assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED + + future += timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ALARM_TRIGGERED + def test_armed_home_with_specific_pending(self): """Test arm home method.""" self.assertTrue(setup_component( @@ -856,6 +1104,78 @@ def test_trigger_with_specific_pending(self): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) + def test_arm_away_after_disabled_disarmed(self): + """Test pending state with and without zero trigger time.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'pending_time': 0, + 'delay_time': 1, + 'armed_away': { + 'pending_time': 1, + }, + 'disarmed': { + 'trigger_time': 0 + }, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_DISARMED, + state.attributes['pre_pending_state']) + self.assertEqual(STATE_ALARM_ARMED_AWAY, + state.attributes['post_pending_state']) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_DISARMED, + state.attributes['pre_pending_state']) + self.assertEqual(STATE_ALARM_ARMED_AWAY, + state.attributes['post_pending_state']) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_AWAY, state.state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_PENDING, state.state) + self.assertEqual(STATE_ALARM_ARMED_AWAY, + state.attributes['pre_pending_state']) + self.assertEqual(STATE_ALARM_TRIGGERED, + state.attributes['post_pending_state']) + + future += timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_TRIGGERED, state.state) + def test_arm_home_via_command_topic(self): """Test arming home via command topic.""" assert setup_component(self.hass, alarm_control_panel.DOMAIN, { From c8e9ff8e82cbbfbabb8d81768f162ec2ed486862 Mon Sep 17 00:00:00 2001 From: Paolo Bonzini Date: Tue, 21 Nov 2017 18:55:46 +0100 Subject: [PATCH 8/8] alarm/manual, alarm/manual_mqtt: add template-based code This lets you specify a code only when disarming, use OTP sensors to generate codes, and much more. The only limit is your imagination! --- .../components/alarm_control_panel/demo.py | 2 +- .../components/alarm_control_panel/manual.py | 23 +++++- .../alarm_control_panel/manual_mqtt.py | 23 +++++- .../alarm_control_panel/test_manual.py | 65 +++++++++++++++ .../alarm_control_panel/test_manual_mqtt.py | 79 ++++++++++++++++++- 5 files changed, 179 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/demo.py b/homeassistant/components/alarm_control_panel/demo.py index 6365c89450297..c080a136c080e 100644 --- a/homeassistant/components/alarm_control_panel/demo.py +++ b/homeassistant/components/alarm_control_panel/demo.py @@ -16,7 +16,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo alarm control panel platform.""" add_devices([ - manual.ManualAlarm(hass, 'Alarm', '1234', False, { + manual.ManualAlarm(hass, 'Alarm', '1234', None, False, { STATE_ALARM_ARMED_AWAY: { CONF_DELAY_TIME: datetime.timedelta(seconds=0), CONF_PENDING_TIME: datetime.timedelta(seconds=5), diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index fea9bd401721d..5ff6092493b38 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -21,6 +21,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time +CONF_CODE_TEMPLATE = 'code_template' + DEFAULT_ALARM_NAME = 'HA Alarm' DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0) DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60) @@ -71,7 +73,8 @@ def _state_schema(state): PLATFORM_SCHEMA = vol.Schema(vol.All({ vol.Required(CONF_PLATFORM): 'manual', vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, - vol.Optional(CONF_CODE): cv.string, + vol.Exclusive(CONF_CODE, 'code validation'): cv.string, + vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template, vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): @@ -103,6 +106,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, config[CONF_NAME], config.get(CONF_CODE), + config.get(CONF_CODE_TEMPLATE), config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER), config )]) @@ -120,13 +124,17 @@ class ManualAlarm(alarm.AlarmControlPanel): A trigger_time of zero disables the alarm_trigger service. """ - def __init__(self, hass, name, code, + def __init__(self, hass, name, code, code_template, disarm_after_trigger, config): """Init the manual alarm panel.""" self._state = STATE_ALARM_DISARMED self._hass = hass self._name = name - self._code = str(code) if code else None + if code_template: + self._code = code_template + self._code.hass = hass + else: + self._code = code or None self._disarm_after_trigger = disarm_after_trigger self._previous_state = self._state self._state_ts = None @@ -267,7 +275,14 @@ def _update_state(self, state): def _validate_code(self, code, state): """Validate given code.""" - check = self._code is None or code == self._code + if self._code is None: + return True + if isinstance(self._code, str): + alarm_code = self._code + else: + alarm_code = self._code.render(from_state=self._state, + to_state=state) + check = not alarm_code or code == alarm_code if not check: _LOGGER.warning("Invalid code given for %s", state) return check diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 6713a77be8d04..9e388806e7349 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -26,6 +26,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time +CONF_CODE_TEMPLATE = 'code_template' + CONF_PAYLOAD_DISARM = 'payload_disarm' CONF_PAYLOAD_ARM_HOME = 'payload_arm_home' CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away' @@ -87,7 +89,8 @@ def _state_schema(state): PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Required(CONF_PLATFORM): 'manual_mqtt', vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, - vol.Optional(CONF_CODE): cv.string, + vol.Exclusive(CONF_CODE, 'code validation'): cv.string, + vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template, vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): @@ -123,6 +126,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, config[CONF_NAME], config.get(CONF_CODE), + config.get(CONF_CODE_TEMPLATE), config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER), config.get(mqtt.CONF_STATE_TOPIC), config.get(mqtt.CONF_COMMAND_TOPIC), @@ -146,7 +150,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): A trigger_time of zero disables the alarm_trigger service. """ - def __init__(self, hass, name, code, + def __init__(self, hass, name, code, code_template, disarm_after_trigger, state_topic, command_topic, qos, payload_disarm, payload_arm_home, payload_arm_away, @@ -155,7 +159,11 @@ def __init__(self, hass, name, code, self._state = STATE_ALARM_DISARMED self._hass = hass self._name = name - self._code = str(code) if code else None + if code_template: + self._code = code_template + self._code.hass = hass + else: + self._code = code or None self._disarm_after_trigger = disarm_after_trigger self._previous_state = self._state self._state_ts = None @@ -297,7 +305,14 @@ def _update_state(self, state): def _validate_code(self, code, state): """Validate given code.""" - check = self._code is None or code == self._code + if self._code is None: + return True + if isinstance(self._code, str): + alarm_code = self._code + else: + alarm_code = self._code.render(from_state=self._state, + to_state=state) + check = not alarm_code or code == alarm_code if not check: _LOGGER.warning("Invalid code given for %s", state) return check diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py index ade55642677bf..c47ed941b6577 100644 --- a/tests/components/alarm_control_panel/test_manual.py +++ b/tests/components/alarm_control_panel/test_manual.py @@ -140,6 +140,32 @@ def test_arm_away_no_pending(self): self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) + def test_arm_home_with_template_code(self): + """Attempt to arm with a template-based code.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code_template': '{{ "abc" }}', + 'pending_time': 0, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.hass.start() + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_home(self.hass, 'abc') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) + def test_arm_away_with_pending(self): """Test arm home method.""" self.assertTrue(setup_component( @@ -1070,6 +1096,45 @@ def test_disarm_during_trigger_with_invalid_code(self): self.assertEqual(STATE_ALARM_TRIGGERED, self.hass.states.get(entity_id).state) + def test_disarm_with_template_code(self): + """Attempt to disarm with a valid or invalid template-based code.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code_template': + '{{ "" if from_state == "disarmed" else "abc" }}', + 'pending_time': 0, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.hass.start() + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_home(self.hass, 'def') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) + + alarm_control_panel.alarm_disarm(self.hass, 'def') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) + + alarm_control_panel.alarm_disarm(self.hass, 'abc') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_DISARMED, state.state) + def test_arm_custom_bypass_no_pending(self): """Test arm custom bypass method.""" self.assertTrue(setup_component( diff --git a/tests/components/alarm_control_panel/test_manual_mqtt.py b/tests/components/alarm_control_panel/test_manual_mqtt.py index 7d7eb7b8175c5..83254d9104f9a 100644 --- a/tests/components/alarm_control_panel/test_manual_mqtt.py +++ b/tests/components/alarm_control_panel/test_manual_mqtt.py @@ -162,6 +162,34 @@ def test_arm_away_no_pending(self): self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) + def test_arm_home_with_template_code(self): + """Attempt to arm with a template-based code.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code_template': '{{ "abc" }}', + 'pending_time': 0, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.hass.start() + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_home(self.hass, 'abc') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) + def test_arm_away_with_pending(self): """Test arm home method.""" self.assertTrue(setup_component( @@ -1109,7 +1137,7 @@ def test_arm_away_after_disabled_disarmed(self): self.assertTrue(setup_component( self.hass, alarm_control_panel.DOMAIN, {'alarm_control_panel': { - 'platform': 'manual', + 'platform': 'manual_mqtt', 'name': 'test', 'code': CODE, 'pending_time': 0, @@ -1120,7 +1148,9 @@ def test_arm_away_after_disabled_disarmed(self): 'disarmed': { 'trigger_time': 0 }, - 'disarm_after_trigger': False + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', }})) entity_id = 'alarm_control_panel.test' @@ -1149,7 +1179,7 @@ def test_arm_away_after_disabled_disarmed(self): state.attributes['post_pending_state']) future = dt_util.utcnow() + timedelta(seconds=1) - with patch(('homeassistant.components.alarm_control_panel.manual.' + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' 'dt_util.utcnow'), return_value=future): fire_time_changed(self.hass, future) self.hass.block_till_done() @@ -1168,7 +1198,7 @@ def test_arm_away_after_disabled_disarmed(self): state.attributes['post_pending_state']) future += timedelta(seconds=1) - with patch(('homeassistant.components.alarm_control_panel.manual.' + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' 'dt_util.utcnow'), return_value=future): fire_time_changed(self.hass, future) self.hass.block_till_done() @@ -1176,6 +1206,47 @@ def test_arm_away_after_disabled_disarmed(self): state = self.hass.states.get(entity_id) self.assertEqual(STATE_ALARM_TRIGGERED, state.state) + def test_disarm_with_template_code(self): + """Attempt to disarm with a valid or invalid template-based code.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code_template': + '{{ "" if from_state == "disarmed" else "abc" }}', + 'pending_time': 0, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.hass.start() + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_home(self.hass, 'def') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) + + alarm_control_panel.alarm_disarm(self.hass, 'def') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) + + alarm_control_panel.alarm_disarm(self.hass, 'abc') + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(STATE_ALARM_DISARMED, state.state) + def test_arm_home_via_command_topic(self): """Test arming home via command topic.""" assert setup_component(self.hass, alarm_control_panel.DOMAIN, {