From 12f1a8f551af6131ecd58da84630cd94dfa9db85 Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 1 Nov 2019 21:36:18 +0100 Subject: [PATCH] Add improved scene support to the alarm_control_panel integration (#28269) * Add improved scene support to the alarm_control_panel integration * Add service description for alarm_arm_custom_bypass --- .../alarm_control_panel/reproduce_state.py | 84 ++++++++++ .../alarm_control_panel/services.yaml | 10 ++ .../test_reproduce_state.py | 148 ++++++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/reproduce_state.py create mode 100644 tests/components/alarm_control_panel/test_reproduce_state.py diff --git a/homeassistant/components/alarm_control_panel/reproduce_state.py b/homeassistant/components/alarm_control_panel/reproduce_state.py new file mode 100644 index 00000000000..705bca608a6 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/reproduce_state.py @@ -0,0 +1,84 @@ +"""Reproduce an Alarm control panel state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = { + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ALARM_ARMED_AWAY: + service = SERVICE_ALARM_ARM_AWAY + elif state.state == STATE_ALARM_ARMED_CUSTOM_BYPASS: + service = SERVICE_ALARM_ARM_CUSTOM_BYPASS + elif state.state == STATE_ALARM_ARMED_HOME: + service = SERVICE_ALARM_ARM_HOME + elif state.state == STATE_ALARM_ARMED_NIGHT: + service = SERVICE_ALARM_ARM_NIGHT + elif state.state == STATE_ALARM_DISARMED: + service = SERVICE_ALARM_DISARM + elif state.state == STATE_ALARM_TRIGGERED: + service = SERVICE_ALARM_TRIGGER + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Alarm control panel states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 7918631464f..9abf2189ed3 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -10,6 +10,16 @@ alarm_disarm: description: An optional code to disarm the alarm control panel with. example: 1234 +alarm_arm_custom_bypass: + description: Send arm custom bypass command. + fields: + entity_id: + description: Name of alarm control panel to arm custom bypass. + example: 'alarm_control_panel.downstairs' + code: + description: An optional code to arm custom bypass the alarm control panel with. + example: 1234 + alarm_arm_home: description: Send the alarm the command for arm home. fields: diff --git a/tests/components/alarm_control_panel/test_reproduce_state.py b/tests/components/alarm_control_panel/test_reproduce_state.py new file mode 100644 index 00000000000..61b0e3ccd30 --- /dev/null +++ b/tests/components/alarm_control_panel/test_reproduce_state.py @@ -0,0 +1,148 @@ +"""Test reproduce state for Alarm control panel.""" +from homeassistant.const import ( + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Alarm control panel states.""" + hass.states.async_set( + "alarm_control_panel.entity_armed_away", STATE_ALARM_ARMED_AWAY, {} + ) + hass.states.async_set( + "alarm_control_panel.entity_armed_custom_bypass", + STATE_ALARM_ARMED_CUSTOM_BYPASS, + {}, + ) + hass.states.async_set( + "alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_HOME, {} + ) + hass.states.async_set( + "alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_NIGHT, {} + ) + hass.states.async_set( + "alarm_control_panel.entity_disarmed", STATE_ALARM_DISARMED, {} + ) + hass.states.async_set( + "alarm_control_panel.entity_triggered", STATE_ALARM_TRIGGERED, {} + ) + + arm_away_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_ARM_AWAY + ) + arm_custom_bypass_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_ARM_CUSTOM_BYPASS + ) + arm_home_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_ARM_HOME + ) + arm_night_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_ARM_NIGHT + ) + disarm_calls = async_mock_service(hass, "alarm_control_panel", SERVICE_ALARM_DISARM) + trigger_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_TRIGGER + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("alarm_control_panel.entity_armed_away", STATE_ALARM_ARMED_AWAY), + State( + "alarm_control_panel.entity_armed_custom_bypass", + STATE_ALARM_ARMED_CUSTOM_BYPASS, + ), + State("alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_HOME), + State("alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_NIGHT), + State("alarm_control_panel.entity_disarmed", STATE_ALARM_DISARMED), + State("alarm_control_panel.entity_triggered", STATE_ALARM_TRIGGERED), + ], + blocking=True, + ) + + assert len(arm_away_calls) == 0 + assert len(arm_custom_bypass_calls) == 0 + assert len(arm_home_calls) == 0 + assert len(arm_night_calls) == 0 + assert len(disarm_calls) == 0 + assert len(trigger_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("alarm_control_panel.entity_triggered", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(arm_away_calls) == 0 + assert len(arm_custom_bypass_calls) == 0 + assert len(arm_home_calls) == 0 + assert len(arm_night_calls) == 0 + assert len(disarm_calls) == 0 + assert len(trigger_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("alarm_control_panel.entity_armed_away", STATE_ALARM_TRIGGERED), + State( + "alarm_control_panel.entity_armed_custom_bypass", STATE_ALARM_ARMED_AWAY + ), + State( + "alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_CUSTOM_BYPASS + ), + State("alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_HOME), + State("alarm_control_panel.entity_disarmed", STATE_ALARM_ARMED_NIGHT), + State("alarm_control_panel.entity_triggered", STATE_ALARM_DISARMED), + # Should not raise + State("alarm_control_panel.non_existing", "on"), + ], + blocking=True, + ) + + assert len(arm_away_calls) == 1 + assert arm_away_calls[0].domain == "alarm_control_panel" + assert arm_away_calls[0].data == { + "entity_id": "alarm_control_panel.entity_armed_custom_bypass" + } + + assert len(arm_custom_bypass_calls) == 1 + assert arm_custom_bypass_calls[0].domain == "alarm_control_panel" + assert arm_custom_bypass_calls[0].data == { + "entity_id": "alarm_control_panel.entity_armed_home" + } + + assert len(arm_home_calls) == 1 + assert arm_home_calls[0].domain == "alarm_control_panel" + assert arm_home_calls[0].data == { + "entity_id": "alarm_control_panel.entity_armed_night" + } + + assert len(arm_night_calls) == 1 + assert arm_night_calls[0].domain == "alarm_control_panel" + assert arm_night_calls[0].data == { + "entity_id": "alarm_control_panel.entity_disarmed" + } + + assert len(disarm_calls) == 1 + assert disarm_calls[0].domain == "alarm_control_panel" + assert disarm_calls[0].data == {"entity_id": "alarm_control_panel.entity_triggered"} + + assert len(trigger_calls) == 1 + assert trigger_calls[0].domain == "alarm_control_panel" + assert trigger_calls[0].data == { + "entity_id": "alarm_control_panel.entity_armed_away" + }