From e62ef067ccc2e8ba5da241ad082b358dbe3513bb Mon Sep 17 00:00:00 2001 From: uchagani Date: Mon, 20 Nov 2017 11:34:21 -0600 Subject: [PATCH] Add Arm Custom Bypass to alarm_control_panel (#10697) --- .../alarm_control_panel/__init__.py | 26 +++- .../components/alarm_control_panel/manual.py | 18 ++- homeassistant/const.py | 3 + .../alarm_control_panel/test_manual.py | 115 +++++++++++++++++- 4 files changed, 156 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 1141e42f9ef..f6fd3f3bea9 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, - SERVICE_ALARM_ARM_NIGHT) + SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) from homeassistant.config import load_yaml_config_file from homeassistant.loader import bind_hass from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa @@ -33,6 +33,7 @@ SERVICE_TO_METHOD = { SERVICE_ALARM_ARM_HOME: 'alarm_arm_home', SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away', SERVICE_ALARM_ARM_NIGHT: 'alarm_arm_night', + SERVICE_ALARM_ARM_CUSTOM_BYPASS: 'alarm_arm_custom_bypass', SERVICE_ALARM_TRIGGER: 'alarm_trigger' } @@ -107,6 +108,18 @@ def alarm_trigger(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) +@bind_hass +def alarm_arm_custom_bypass(hass, code=None, entity_id=None): + """Send the alarm the command for arm custom bypass.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data) + + @asyncio.coroutine def async_setup(hass, config): """Track states and offer events for sensors.""" @@ -216,6 +229,17 @@ class AlarmControlPanel(Entity): """ return self.hass.async_add_job(self.alarm_trigger, code) + def alarm_arm_custom_bypass(self, code=None): + """Send arm custom bypass command.""" + raise NotImplementedError() + + def async_alarm_arm_custom_bypass(self, code=None): + """Send arm custom bypass command. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(self.alarm_arm_custom_bypass, code) + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 237959ab10d..55f3834c06a 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -14,9 +14,9 @@ import homeassistant.components.alarm_control_panel as alarm import homeassistant.util.dt as dt_util 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) + 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) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time @@ -26,7 +26,8 @@ DEFAULT_TRIGGER_TIME = 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_NIGHT, STATE_ALARM_TRIGGERED, + STATE_ALARM_ARMED_CUSTOM_BYPASS] ATTR_POST_PENDING_STATE = 'post_pending_state' @@ -59,6 +60,8 @@ PLATFORM_SCHEMA = vol.Schema(vol.All({ 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, }, _state_validator)) @@ -174,6 +177,13 @@ class ManualAlarm(alarm.AlarmControlPanel): self._update_state(STATE_ALARM_ARMED_NIGHT) + def alarm_arm_custom_bypass(self, code=None): + """Send arm custom bypass command.""" + if not self._validate_code(code, STATE_ALARM_ARMED_CUSTOM_BYPASS): + return + + self._update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS) + def alarm_trigger(self, code=None): """Send alarm trigger command. No code needed.""" self._pre_trigger_state = self._state diff --git a/homeassistant/const.py b/homeassistant/const.py index eeff5773640..f46058b186c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -181,6 +181,7 @@ STATE_ALARM_DISARMED = 'disarmed' STATE_ALARM_ARMED_HOME = 'armed_home' STATE_ALARM_ARMED_AWAY = 'armed_away' STATE_ALARM_ARMED_NIGHT = 'armed_night' +STATE_ALARM_ARMED_CUSTOM_BYPASS = 'armed_custom_bypass' STATE_ALARM_PENDING = 'pending' STATE_ALARM_ARMING = 'arming' STATE_ALARM_DISARMING = 'disarming' @@ -347,8 +348,10 @@ SERVICE_ALARM_DISARM = 'alarm_disarm' SERVICE_ALARM_ARM_HOME = 'alarm_arm_home' SERVICE_ALARM_ARM_AWAY = 'alarm_arm_away' SERVICE_ALARM_ARM_NIGHT = 'alarm_arm_night' +SERVICE_ALARM_ARM_CUSTOM_BYPASS = 'alarm_arm_custom_bypass' SERVICE_ALARM_TRIGGER = 'alarm_trigger' + SERVICE_LOCK = 'lock' SERVICE_UNLOCK = 'unlock' diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py index 1b10b942281..2e96b81bfce 100644 --- a/tests/components/alarm_control_panel/test_manual.py +++ b/tests/components/alarm_control_panel/test_manual.py @@ -6,7 +6,8 @@ from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) from homeassistant.components import alarm_control_panel import homeassistant.util.dt as dt_util @@ -673,3 +674,115 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_TRIGGERED, self.hass.states.get(entity_id).state) + + def test_arm_custom_bypass_no_pending(self): + """Test arm custom bypass method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + '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_custom_bypass(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_CUSTOM_BYPASS, + self.hass.states.get(entity_id).state) + + def test_arm_custom_bypass_with_pending(self): + """Test arm custom bypass method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + '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_custom_bypass(self.hass, CODE, entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + state = self.hass.states.get(entity_id) + assert state.attributes['post_pending_state'] == \ + STATE_ALARM_ARMED_CUSTOM_BYPASS + + 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_ARMED_CUSTOM_BYPASS + + def test_arm_custom_bypass_with_invalid_code(self): + """Attempt to custom bypass without a valid code.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + '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_custom_bypass(self.hass, CODE + '2') + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_armed_custom_bypass_with_specific_pending(self): + """Test arm custom bypass method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'pending_time': 10, + 'armed_custom_bypass': { + 'pending_time': 2 + } + }})) + + entity_id = 'alarm_control_panel.test' + + alarm_control_panel.alarm_arm_custom_bypass(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=2) + 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_ARMED_CUSTOM_BYPASS, + self.hass.states.get(entity_id).state)