From 6b7ff2bf4428e10bb907b714de1a305c5f755f35 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 29 May 2024 10:46:53 +0200 Subject: [PATCH] Add default code to alarm_control_panel (#112540) --- .../alarm_control_panel/__init__.py | 108 ++++++++- .../components/canary/alarm_control_panel.py | 1 + .../components/demo/alarm_control_panel.py | 8 +- .../components/freebox/alarm_control_panel.py | 2 + .../homematicip_cloud/alarm_control_panel.py | 1 + .../totalconnect/alarm_control_panel.py | 1 + .../alarm_control_panel/conftest.py | 181 ++++++++++++++- .../alarm_control_panel/test_init.py | 206 +++++++++++++++++ .../test_alarm_control_panel.py | 8 +- .../manual/test_alarm_control_panel.py | 2 +- .../manual_mqtt/test_alarm_control_panel.py | 8 +- .../mqtt/test_alarm_control_panel.py | 214 +++++++++++++----- .../template/test_alarm_control_panel.py | 10 +- .../snapshots/test_alarm_control_panel.ambr | 4 +- 14 files changed, 680 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 3260454826a..48ea72c46d9 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -21,7 +21,8 @@ from homeassistant.const import ( SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ServiceValidationError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.deprecation import ( @@ -55,6 +56,8 @@ _LOGGER: Final = logging.getLogger(__name__) SCAN_INTERVAL: Final = timedelta(seconds=30) ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" +CONF_DEFAULT_CODE = "default_code" + ALARM_SERVICE_SCHEMA: Final = make_entity_service_schema( {vol.Optional(ATTR_CODE): cv.string} ) @@ -74,36 +77,38 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_setup(config) component.async_register_entity_service( - SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, "async_alarm_disarm" + SERVICE_ALARM_DISARM, + ALARM_SERVICE_SCHEMA, + "async_handle_alarm_disarm", ) component.async_register_entity_service( SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, - "async_alarm_arm_home", + "async_handle_alarm_arm_home", [AlarmControlPanelEntityFeature.ARM_HOME], ) component.async_register_entity_service( SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, - "async_alarm_arm_away", + "async_handle_alarm_arm_away", [AlarmControlPanelEntityFeature.ARM_AWAY], ) component.async_register_entity_service( SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, - "async_alarm_arm_night", + "async_handle_alarm_arm_night", [AlarmControlPanelEntityFeature.ARM_NIGHT], ) component.async_register_entity_service( SERVICE_ALARM_ARM_VACATION, ALARM_SERVICE_SCHEMA, - "async_alarm_arm_vacation", + "async_handle_alarm_arm_vacation", [AlarmControlPanelEntityFeature.ARM_VACATION], ) component.async_register_entity_service( SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA, - "async_alarm_arm_custom_bypass", + "async_handle_alarm_arm_custom_bypass", [AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS], ) component.async_register_entity_service( @@ -150,6 +155,21 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A _attr_supported_features: AlarmControlPanelEntityFeature = ( AlarmControlPanelEntityFeature(0) ) + _alarm_control_panel_option_default_code: str | None = None + + @final + @callback + def code_or_default_code(self, code: str | None) -> str | None: + """Return code to use for a service call. + + If the passed in code is not None, it will be returned. Otherwise return the + default code, if set, or None if not set, is returned. + """ + if code: + # Return code provided by user + return code + # Fallback to default code or None if not set + return self._alarm_control_panel_option_default_code @cached_property def code_format(self) -> CodeFormat | None: @@ -166,6 +186,26 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A """Whether the code is required for arm actions.""" return self._attr_code_arm_required + @final + @callback + def check_code_arm_required(self, code: str | None) -> str | None: + """Check if arm code is required, raise if no code is given.""" + if not (_code := self.code_or_default_code(code)) and self.code_arm_required: + raise ServiceValidationError( + f"Arming requires a code but none was given for {self.entity_id}", + translation_domain=DOMAIN, + translation_key="code_arm_required", + translation_placeholders={ + "entity_id": self.entity_id, + }, + ) + return _code + + @final + async def async_handle_alarm_disarm(self, code: str | None = None) -> None: + """Add default code and disarm.""" + await self.async_alarm_disarm(self.code_or_default_code(code)) + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" raise NotImplementedError @@ -174,6 +214,11 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A """Send disarm command.""" await self.hass.async_add_executor_job(self.alarm_disarm, code) + @final + async def async_handle_alarm_arm_home(self, code: str | None = None) -> None: + """Add default code and arm home.""" + await self.async_alarm_arm_home(self.check_code_arm_required(code)) + def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" raise NotImplementedError @@ -182,6 +227,11 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A """Send arm home command.""" await self.hass.async_add_executor_job(self.alarm_arm_home, code) + @final + async def async_handle_alarm_arm_away(self, code: str | None = None) -> None: + """Add default code and arm away.""" + await self.async_alarm_arm_away(self.check_code_arm_required(code)) + def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" raise NotImplementedError @@ -190,6 +240,11 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A """Send arm away command.""" await self.hass.async_add_executor_job(self.alarm_arm_away, code) + @final + async def async_handle_alarm_arm_night(self, code: str | None = None) -> None: + """Add default code and arm night.""" + await self.async_alarm_arm_night(self.check_code_arm_required(code)) + def alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" raise NotImplementedError @@ -198,6 +253,11 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A """Send arm night command.""" await self.hass.async_add_executor_job(self.alarm_arm_night, code) + @final + async def async_handle_alarm_arm_vacation(self, code: str | None = None) -> None: + """Add default code and arm vacation.""" + await self.async_alarm_arm_vacation(self.check_code_arm_required(code)) + def alarm_arm_vacation(self, code: str | None = None) -> None: """Send arm vacation command.""" raise NotImplementedError @@ -214,6 +274,13 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A """Send alarm trigger command.""" await self.hass.async_add_executor_job(self.alarm_trigger, code) + @final + async def async_handle_alarm_arm_custom_bypass( + self, code: str | None = None + ) -> None: + """Add default code and arm custom bypass.""" + await self.async_alarm_arm_custom_bypass(self.check_code_arm_required(code)) + def alarm_arm_custom_bypass(self, code: str | None = None) -> None: """Send arm custom bypass command.""" raise NotImplementedError @@ -242,6 +309,33 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A ATTR_CODE_ARM_REQUIRED: self.code_arm_required, } + async def async_internal_added_to_hass(self) -> None: + """Call when the alarm control panel entity is added to hass.""" + await super().async_internal_added_to_hass() + if not self.registry_entry: + return + self._async_read_entity_options() + + @callback + def async_registry_entry_updated(self) -> None: + """Run when the entity registry entry has been updated.""" + self._async_read_entity_options() + + @callback + def _async_read_entity_options(self) -> None: + """Read entity options from entity registry. + + Called when the entity registry entry has been updated and before the + alarm control panel is added to the state machine. + """ + assert self.registry_entry + if (alarm_options := self.registry_entry.options.get(DOMAIN)) and ( + default_code := alarm_options.get(CONF_DEFAULT_CODE) + ): + self._alarm_control_panel_option_default_code = default_code + return + self._alarm_control_panel_option_default_code = None + # As we import constants of the const module here, we need to add the following # functions to check for deprecated constants again diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 445579b9e4a..a7d5dc8ab98 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -53,6 +53,7 @@ class CanaryAlarm( | AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_NIGHT ) + _attr_code_arm_required = False def __init__( self, coordinator: CanaryDataUpdateCoordinator, location: Location diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py index 0b152f87c29..f95042f2cc7 100644 --- a/homeassistant/components/demo/alarm_control_panel.py +++ b/homeassistant/components/demo/alarm_control_panel.py @@ -30,7 +30,7 @@ async def async_setup_entry( """Set up the Demo config entry.""" async_add_entities( [ - ManualAlarm( # type:ignore[no-untyped-call] + DemoAlarm( # type:ignore[no-untyped-call] hass, "Security", "1234", @@ -74,3 +74,9 @@ async def async_setup_entry( ) ] ) + + +class DemoAlarm(ManualAlarm): + """Demo Alarm Control Panel.""" + + _attr_unique_id = "demo_alarm_control_panel" diff --git a/homeassistant/components/freebox/alarm_control_panel.py b/homeassistant/components/freebox/alarm_control_panel.py index 4c62b928dff..da5983f9374 100644 --- a/homeassistant/components/freebox/alarm_control_panel.py +++ b/homeassistant/components/freebox/alarm_control_panel.py @@ -52,6 +52,8 @@ async def async_setup_entry( class FreeboxAlarm(FreeboxHomeEntity, AlarmControlPanelEntity): """Representation of a Freebox alarm.""" + _attr_code_arm_required = False + def __init__( self, hass: HomeAssistant, router: FreeboxRouter, node: dict[str, Any] ) -> None: diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 2913896d511..1f294a8cade 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -47,6 +47,7 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) + _attr_code_arm_required = False def __init__(self, hap: HomematicipHAP) -> None: """Initialize the alarm control panel.""" diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 511a0fd6270..17a16674dd5 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -74,6 +74,7 @@ class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_NIGHT ) + _attr_code_arm_required = False def __init__( self, diff --git a/tests/components/alarm_control_panel/conftest.py b/tests/components/alarm_control_panel/conftest.py index cda3d81b26e..c076dd8ab67 100644 --- a/tests/components/alarm_control_panel/conftest.py +++ b/tests/components/alarm_control_panel/conftest.py @@ -1,8 +1,33 @@ """Fixturs for Alarm Control Panel tests.""" +from collections.abc import Generator +from unittest.mock import MagicMock + import pytest -from tests.components.alarm_control_panel.common import MockAlarm +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, + AlarmControlPanelEntity, + AlarmControlPanelEntityFeature, +) +from homeassistant.components.alarm_control_panel.const import CodeFormat +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .common import MockAlarm + +from tests.common import ( + MockConfigEntry, + MockModule, + MockPlatform, + mock_config_flow, + mock_integration, + mock_platform, +) + +TEST_DOMAIN = "test" @pytest.fixture @@ -20,3 +45,157 @@ def mock_alarm_control_panel_entities() -> dict[str, MockAlarm]: unique_id="unique_no_arm_code", ), } + + +class MockAlarmControlPanel(AlarmControlPanelEntity): + """Mocked alarm control entity.""" + + def __init__( + self, + supported_features: AlarmControlPanelEntityFeature = AlarmControlPanelEntityFeature( + 0 + ), + code_format: CodeFormat | None = None, + code_arm_required: bool = True, + ) -> None: + """Initialize the alarm control.""" + self.calls_disarm = MagicMock() + self.calls_arm_home = MagicMock() + self.calls_arm_away = MagicMock() + self.calls_arm_night = MagicMock() + self.calls_arm_vacation = MagicMock() + self.calls_trigger = MagicMock() + self.calls_arm_custom = MagicMock() + self._attr_code_format = code_format + self._attr_supported_features = supported_features + self._attr_code_arm_required = code_arm_required + self._attr_has_entity_name = True + self._attr_name = "test_alarm_control_panel" + self._attr_unique_id = "very_unique_alarm_control_panel_id" + super().__init__() + + def alarm_disarm(self, code: str | None = None) -> None: + """Mock alarm disarm calls.""" + self.calls_disarm(code) + + def alarm_arm_home(self, code: str | None = None) -> None: + """Mock arm home calls.""" + self.calls_arm_home(code) + + def alarm_arm_away(self, code: str | None = None) -> None: + """Mock arm away calls.""" + self.calls_arm_away(code) + + def alarm_arm_night(self, code: str | None = None) -> None: + """Mock arm night calls.""" + self.calls_arm_night(code) + + def alarm_arm_vacation(self, code: str | None = None) -> None: + """Mock arm vacation calls.""" + self.calls_arm_vacation(code) + + def alarm_trigger(self, code: str | None = None) -> None: + """Mock trigger calls.""" + self.calls_trigger(code) + + def alarm_arm_custom_bypass(self, code: str | None = None) -> None: + """Mock arm custom bypass calls.""" + self.calls_arm_custom(code) + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +@pytest.fixture(autouse=True) +def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: + """Mock config flow.""" + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + with mock_config_flow(TEST_DOMAIN, MockFlow): + yield + + +@pytest.fixture +async def code_format() -> CodeFormat | None: + """Return the code format for the test alarm control panel entity.""" + return CodeFormat.NUMBER + + +@pytest.fixture +async def code_arm_required() -> bool: + """Return if code required for arming.""" + return True + + +@pytest.fixture(name="supported_features") +async def lock_supported_features() -> AlarmControlPanelEntityFeature: + """Return the supported features for the test alarm control panel entity.""" + return ( + AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS + | AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_NIGHT + | AlarmControlPanelEntityFeature.ARM_VACATION + | AlarmControlPanelEntityFeature.TRIGGER + ) + + +@pytest.fixture(name="mock_alarm_control_panel_entity") +async def setup_lock_platform_test_entity( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + code_format: CodeFormat | None, + supported_features: AlarmControlPanelEntityFeature, + code_arm_required: bool, +) -> MagicMock: + """Set up alarm control panel entity using an entity platform.""" + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setup( + config_entry, ALARM_CONTROL_PANEL_DOMAIN + ) + return True + + MockPlatform(hass, f"{TEST_DOMAIN}.config_flow") + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + ), + ) + + # Unnamed sensor without device class -> no name + entity = MockAlarmControlPanel( + supported_features=supported_features, + code_format=code_format, + code_arm_required=code_arm_required, + ) + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test alarm control panel platform via config entry.""" + async_add_entities([entity]) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{ALARM_CONTROL_PANEL_DOMAIN}", + MockPlatform(async_setup_entry=async_setup_entry_platform), + ) + + config_entry = MockConfigEntry(domain=TEST_DOMAIN) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + assert state is not None + + return entity diff --git a/tests/components/alarm_control_panel/test_init.py b/tests/components/alarm_control_panel/test_init.py index 42a532cbb1a..06724978ce3 100644 --- a/tests/components/alarm_control_panel/test_init.py +++ b/tests/components/alarm_control_panel/test_init.py @@ -1,14 +1,52 @@ """Test for the alarm control panel const module.""" from types import ModuleType +from typing import Any import pytest from homeassistant.components import alarm_control_panel +from homeassistant.components.alarm_control_panel.const import ( + AlarmControlPanelEntityFeature, + CodeFormat, +) +from homeassistant.const import ( + ATTR_CODE, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_ARM_VACATION, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.typing import UNDEFINED, UndefinedType + +from .conftest import MockAlarmControlPanel from tests.common import help_test_all, import_and_test_deprecated_constant_enum +async def help_test_async_alarm_control_panel_service( + hass: HomeAssistant, + entity_id: str, + service: str, + code: str | None | UndefinedType = UNDEFINED, +) -> None: + """Help to lock a test lock.""" + data: dict[str, Any] = {"entity_id": entity_id} + if code is not UNDEFINED: + data[ATTR_CODE] = code + + await hass.services.async_call( + alarm_control_panel.DOMAIN, service, data, blocking=True + ) + await hass.async_block_till_done() + + @pytest.mark.parametrize( "module", [alarm_control_panel, alarm_control_panel.const], @@ -77,3 +115,171 @@ def test_deprecated_supported_features_ints(caplog: pytest.LogCaptureFixture) -> is alarm_control_panel.AlarmControlPanelEntityFeature(1) ) assert "is using deprecated supported features values" not in caplog.text + + +async def test_set_mock_alarm_control_panel_options( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_alarm_control_panel_entity: MockAlarmControlPanel, +) -> None: + """Test mock attributes and default code stored in the registry.""" + entity_registry.async_update_entity_options( + "alarm_control_panel.test_alarm_control_panel", + "alarm_control_panel", + {alarm_control_panel.CONF_DEFAULT_CODE: "1234"}, + ) + await hass.async_block_till_done() + + assert ( + mock_alarm_control_panel_entity._alarm_control_panel_option_default_code + == "1234" + ) + state = hass.states.get(mock_alarm_control_panel_entity.entity_id) + assert state is not None + assert state.attributes["code_format"] == CodeFormat.NUMBER + assert ( + state.attributes["supported_features"] + == AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS + | AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_NIGHT + | AlarmControlPanelEntityFeature.ARM_VACATION + | AlarmControlPanelEntityFeature.TRIGGER + ) + + +async def test_default_code_option_update( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_alarm_control_panel_entity: MockAlarmControlPanel, +) -> None: + """Test default code stored in the registry is updated.""" + + assert ( + mock_alarm_control_panel_entity._alarm_control_panel_option_default_code is None + ) + + entity_registry.async_update_entity_options( + "alarm_control_panel.test_alarm_control_panel", + "alarm_control_panel", + {alarm_control_panel.CONF_DEFAULT_CODE: "4321"}, + ) + await hass.async_block_till_done() + + assert ( + mock_alarm_control_panel_entity._alarm_control_panel_option_default_code + == "4321" + ) + + +@pytest.mark.parametrize( + ("code_format", "supported_features"), + [(CodeFormat.TEXT, AlarmControlPanelEntityFeature.ARM_AWAY)], +) +async def test_alarm_control_panel_arm_with_code( + hass: HomeAssistant, mock_alarm_control_panel_entity: MockAlarmControlPanel +) -> None: + """Test alarm control panel entity with open service.""" + state = hass.states.get(mock_alarm_control_panel_entity.entity_id) + assert state.attributes["code_format"] == CodeFormat.TEXT + + with pytest.raises(ServiceValidationError): + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_ARM_AWAY + ) + with pytest.raises(ServiceValidationError): + await help_test_async_alarm_control_panel_service( + hass, + mock_alarm_control_panel_entity.entity_id, + SERVICE_ALARM_ARM_AWAY, + code="", + ) + await help_test_async_alarm_control_panel_service( + hass, + mock_alarm_control_panel_entity.entity_id, + SERVICE_ALARM_ARM_AWAY, + code="1234", + ) + assert mock_alarm_control_panel_entity.calls_arm_away.call_count == 1 + mock_alarm_control_panel_entity.calls_arm_away.assert_called_with("1234") + + +@pytest.mark.parametrize( + ("code_format", "code_arm_required"), + [(CodeFormat.NUMBER, False)], +) +async def test_alarm_control_panel_with_no_code( + hass: HomeAssistant, mock_alarm_control_panel_entity: MockAlarmControlPanel +) -> None: + """Test alarm control panel entity without code.""" + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_ARM_AWAY + ) + mock_alarm_control_panel_entity.calls_arm_away.assert_called_with(None) + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_ARM_CUSTOM_BYPASS + ) + mock_alarm_control_panel_entity.calls_arm_custom.assert_called_with(None) + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_ARM_HOME + ) + mock_alarm_control_panel_entity.calls_arm_home.assert_called_with(None) + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_ARM_NIGHT + ) + mock_alarm_control_panel_entity.calls_arm_night.assert_called_with(None) + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_ARM_VACATION + ) + mock_alarm_control_panel_entity.calls_arm_vacation.assert_called_with(None) + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_DISARM + ) + mock_alarm_control_panel_entity.calls_disarm.assert_called_with(None) + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_TRIGGER + ) + mock_alarm_control_panel_entity.calls_trigger.assert_called_with(None) + + +@pytest.mark.parametrize( + ("code_format", "code_arm_required"), + [(CodeFormat.NUMBER, True)], +) +async def test_alarm_control_panel_with_default_code( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_alarm_control_panel_entity: MockAlarmControlPanel, +) -> None: + """Test alarm control panel entity without code.""" + entity_registry.async_update_entity_options( + "alarm_control_panel.test_alarm_control_panel", + "alarm_control_panel", + {alarm_control_panel.CONF_DEFAULT_CODE: "1234"}, + ) + await hass.async_block_till_done() + + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_ARM_AWAY + ) + mock_alarm_control_panel_entity.calls_arm_away.assert_called_with("1234") + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_ARM_CUSTOM_BYPASS + ) + mock_alarm_control_panel_entity.calls_arm_custom.assert_called_with("1234") + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_ARM_HOME + ) + mock_alarm_control_panel_entity.calls_arm_home.assert_called_with("1234") + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_ARM_NIGHT + ) + mock_alarm_control_panel_entity.calls_arm_night.assert_called_with("1234") + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_ARM_VACATION + ) + mock_alarm_control_panel_entity.calls_arm_vacation.assert_called_with("1234") + await help_test_async_alarm_control_panel_service( + hass, mock_alarm_control_panel_entity.entity_id, SERVICE_ALARM_DISARM + ) + mock_alarm_control_panel_entity.calls_disarm.assert_called_with("1234") diff --git a/tests/components/homekit_controller/test_alarm_control_panel.py b/tests/components/homekit_controller/test_alarm_control_panel.py index a660e29ca17..a8852aac4f7 100644 --- a/tests/components/homekit_controller/test_alarm_control_panel.py +++ b/tests/components/homekit_controller/test_alarm_control_panel.py @@ -34,7 +34,7 @@ async def test_switch_change_alarm_state(hass: HomeAssistant) -> None: await hass.services.async_call( "alarm_control_panel", "alarm_arm_home", - {"entity_id": "alarm_control_panel.testdevice"}, + {"entity_id": "alarm_control_panel.testdevice", "code": "1234"}, blocking=True, ) helper.async_assert_service_values( @@ -47,7 +47,7 @@ async def test_switch_change_alarm_state(hass: HomeAssistant) -> None: await hass.services.async_call( "alarm_control_panel", "alarm_arm_away", - {"entity_id": "alarm_control_panel.testdevice"}, + {"entity_id": "alarm_control_panel.testdevice", "code": "1234"}, blocking=True, ) helper.async_assert_service_values( @@ -60,7 +60,7 @@ async def test_switch_change_alarm_state(hass: HomeAssistant) -> None: await hass.services.async_call( "alarm_control_panel", "alarm_arm_night", - {"entity_id": "alarm_control_panel.testdevice"}, + {"entity_id": "alarm_control_panel.testdevice", "code": "1234"}, blocking=True, ) helper.async_assert_service_values( @@ -73,7 +73,7 @@ async def test_switch_change_alarm_state(hass: HomeAssistant) -> None: await hass.services.async_call( "alarm_control_panel", "alarm_disarm", - {"entity_id": "alarm_control_panel.testdevice"}, + {"entity_id": "alarm_control_panel.testdevice", "code": "1234"}, blocking=True, ) helper.async_assert_service_values( diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index 7a264134320..5910cc3ec9b 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -315,7 +315,7 @@ async def test_with_specific_pending( await hass.services.async_call( alarm_control_panel.DOMAIN, service, - {ATTR_ENTITY_ID: "alarm_control_panel.test"}, + {ATTR_ENTITY_ID: "alarm_control_panel.test", ATTR_CODE: "1234"}, blocking=True, ) diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 5c2704db937..a1c913135a7 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -380,7 +380,7 @@ async def test_with_specific_pending( await hass.services.async_call( alarm_control_panel.DOMAIN, service, - {ATTR_ENTITY_ID: "alarm_control_panel.test"}, + {ATTR_ENTITY_ID: "alarm_control_panel.test", ATTR_CODE: "1234"}, blocking=True, ) @@ -1442,7 +1442,7 @@ async def test_state_changes_are_published_to_mqtt( mqtt_mock.async_publish.reset_mock() # Arm in home mode - await common.async_alarm_arm_home(hass) + await common.async_alarm_arm_home(hass, "1234") await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( "alarm/state", STATE_ALARM_PENDING, 0, True @@ -1462,7 +1462,7 @@ async def test_state_changes_are_published_to_mqtt( mqtt_mock.async_publish.reset_mock() # Arm in away mode - await common.async_alarm_arm_away(hass) + await common.async_alarm_arm_away(hass, "1234") await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( "alarm/state", STATE_ALARM_PENDING, 0, True @@ -1482,7 +1482,7 @@ async def test_state_changes_are_published_to_mqtt( mqtt_mock.async_publish.reset_mock() # Arm in night mode - await common.async_alarm_arm_night(hass) + await common.async_alarm_arm_night(hass, "1234") await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( "alarm/state", STATE_ALARM_PENDING, 0, True diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index b9a65fa2d3d..df226de7002 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -1,5 +1,6 @@ """The tests the MQTT alarm control panel component.""" +from contextlib import AbstractContextManager, contextmanager import copy import json from typing import Any @@ -37,7 +38,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from .test_common import ( help_custom_config, @@ -97,6 +98,17 @@ DEFAULT_CONFIG = { } } +DEFAULT_CONFIG_CODE_NOT_REQUIRED = { + mqtt.DOMAIN: { + alarm_control_panel.DOMAIN: { + "name": "test", + "state_topic": "alarm/state", + "command_topic": "alarm/command", + "code_arm_required": False, + } + } +} + DEFAULT_CONFIG_CODE = { mqtt.DOMAIN: { alarm_control_panel.DOMAIN: { @@ -134,6 +146,12 @@ DEFAULT_CONFIG_REMOTE_CODE_TEXT = { } +@contextmanager +def does_not_raise(): + """Do not raise error.""" + yield + + @pytest.mark.parametrize( ("hass_config", "valid"), [ @@ -317,13 +335,17 @@ async def test_supported_features( @pytest.mark.parametrize( ("hass_config", "service", "payload"), [ - (DEFAULT_CONFIG, SERVICE_ALARM_ARM_HOME, "ARM_HOME"), - (DEFAULT_CONFIG, SERVICE_ALARM_ARM_AWAY, "ARM_AWAY"), - (DEFAULT_CONFIG, SERVICE_ALARM_ARM_NIGHT, "ARM_NIGHT"), - (DEFAULT_CONFIG, SERVICE_ALARM_ARM_VACATION, "ARM_VACATION"), - (DEFAULT_CONFIG, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "ARM_CUSTOM_BYPASS"), - (DEFAULT_CONFIG, SERVICE_ALARM_DISARM, "DISARM"), - (DEFAULT_CONFIG, SERVICE_ALARM_TRIGGER, "TRIGGER"), + (DEFAULT_CONFIG_CODE_NOT_REQUIRED, SERVICE_ALARM_ARM_HOME, "ARM_HOME"), + (DEFAULT_CONFIG_CODE_NOT_REQUIRED, SERVICE_ALARM_ARM_AWAY, "ARM_AWAY"), + (DEFAULT_CONFIG_CODE_NOT_REQUIRED, SERVICE_ALARM_ARM_NIGHT, "ARM_NIGHT"), + (DEFAULT_CONFIG_CODE_NOT_REQUIRED, SERVICE_ALARM_ARM_VACATION, "ARM_VACATION"), + ( + DEFAULT_CONFIG_CODE_NOT_REQUIRED, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "ARM_CUSTOM_BYPASS", + ), + (DEFAULT_CONFIG_CODE_NOT_REQUIRED, SERVICE_ALARM_DISARM, "DISARM"), + (DEFAULT_CONFIG_CODE_NOT_REQUIRED, SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) async def test_publish_mqtt_no_code( @@ -346,34 +368,61 @@ async def test_publish_mqtt_no_code( @pytest.mark.parametrize( - ("hass_config", "service", "payload"), + ("hass_config", "service", "payload", "raises"), [ - (DEFAULT_CONFIG_CODE, SERVICE_ALARM_ARM_HOME, "ARM_HOME"), - (DEFAULT_CONFIG_CODE, SERVICE_ALARM_ARM_AWAY, "ARM_AWAY"), - (DEFAULT_CONFIG_CODE, SERVICE_ALARM_ARM_NIGHT, "ARM_NIGHT"), - (DEFAULT_CONFIG_CODE, SERVICE_ALARM_ARM_VACATION, "ARM_VACATION"), - (DEFAULT_CONFIG_CODE, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "ARM_CUSTOM_BYPASS"), - (DEFAULT_CONFIG_CODE, SERVICE_ALARM_DISARM, "DISARM"), - (DEFAULT_CONFIG_CODE, SERVICE_ALARM_TRIGGER, "TRIGGER"), + ( + DEFAULT_CONFIG_CODE, + SERVICE_ALARM_ARM_HOME, + "ARM_HOME", + pytest.raises(ServiceValidationError), + ), + ( + DEFAULT_CONFIG_CODE, + SERVICE_ALARM_ARM_AWAY, + "ARM_AWAY", + pytest.raises(ServiceValidationError), + ), + ( + DEFAULT_CONFIG_CODE, + SERVICE_ALARM_ARM_NIGHT, + "ARM_NIGHT", + pytest.raises(ServiceValidationError), + ), + ( + DEFAULT_CONFIG_CODE, + SERVICE_ALARM_ARM_VACATION, + "ARM_VACATION", + pytest.raises(ServiceValidationError), + ), + ( + DEFAULT_CONFIG_CODE, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "ARM_CUSTOM_BYPASS", + pytest.raises(ServiceValidationError), + ), + (DEFAULT_CONFIG_CODE, SERVICE_ALARM_DISARM, "DISARM", does_not_raise()), + (DEFAULT_CONFIG_CODE, SERVICE_ALARM_TRIGGER, "TRIGGER", does_not_raise()), ], ) async def test_publish_mqtt_with_code( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - service, - payload, + service: str, + payload: str, + raises: AbstractContextManager, ) -> None: """Test publishing of MQTT messages when code is configured.""" mqtt_mock = await mqtt_mock_entry() call_count = mqtt_mock.async_publish.call_count # No code provided, should not publish - await hass.services.async_call( - alarm_control_panel.DOMAIN, - service, - {ATTR_ENTITY_ID: "alarm_control_panel.test"}, - blocking=True, - ) + with raises: + await hass.services.async_call( + alarm_control_panel.DOMAIN, + service, + {ATTR_ENTITY_ID: "alarm_control_panel.test"}, + blocking=True, + ) assert mqtt_mock.async_publish.call_count == call_count # Wrong code provided, should not publish @@ -396,38 +445,66 @@ async def test_publish_mqtt_with_code( @pytest.mark.parametrize( - ("hass_config", "service", "payload"), + ("hass_config", "service", "payload", "raises"), [ - (DEFAULT_CONFIG_REMOTE_CODE, SERVICE_ALARM_ARM_HOME, "ARM_HOME"), - (DEFAULT_CONFIG_REMOTE_CODE, SERVICE_ALARM_ARM_AWAY, "ARM_AWAY"), - (DEFAULT_CONFIG_REMOTE_CODE, SERVICE_ALARM_ARM_NIGHT, "ARM_NIGHT"), - (DEFAULT_CONFIG_REMOTE_CODE, SERVICE_ALARM_ARM_VACATION, "ARM_VACATION"), + ( + DEFAULT_CONFIG_REMOTE_CODE, + SERVICE_ALARM_ARM_HOME, + "ARM_HOME", + pytest.raises(ServiceValidationError), + ), + ( + DEFAULT_CONFIG_REMOTE_CODE, + SERVICE_ALARM_ARM_AWAY, + "ARM_AWAY", + pytest.raises(ServiceValidationError), + ), + ( + DEFAULT_CONFIG_REMOTE_CODE, + SERVICE_ALARM_ARM_NIGHT, + "ARM_NIGHT", + pytest.raises(ServiceValidationError), + ), + ( + DEFAULT_CONFIG_REMOTE_CODE, + SERVICE_ALARM_ARM_VACATION, + "ARM_VACATION", + pytest.raises(ServiceValidationError), + ), ( DEFAULT_CONFIG_REMOTE_CODE, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "ARM_CUSTOM_BYPASS", + pytest.raises(ServiceValidationError), + ), + (DEFAULT_CONFIG_REMOTE_CODE, SERVICE_ALARM_DISARM, "DISARM", does_not_raise()), + ( + DEFAULT_CONFIG_REMOTE_CODE, + SERVICE_ALARM_TRIGGER, + "TRIGGER", + does_not_raise(), ), - (DEFAULT_CONFIG_REMOTE_CODE, SERVICE_ALARM_DISARM, "DISARM"), - (DEFAULT_CONFIG_REMOTE_CODE, SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) async def test_publish_mqtt_with_remote_code( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, - service, - payload, + service: str, + payload: str, + raises: AbstractContextManager, ) -> None: """Test publishing of MQTT messages when remode code is configured.""" mqtt_mock = await mqtt_mock_entry() call_count = mqtt_mock.async_publish.call_count # No code provided, should not publish - await hass.services.async_call( - alarm_control_panel.DOMAIN, - service, - {ATTR_ENTITY_ID: "alarm_control_panel.test"}, - blocking=True, - ) + with raises: + await hass.services.async_call( + alarm_control_panel.DOMAIN, + service, + {ATTR_ENTITY_ID: "alarm_control_panel.test"}, + blocking=True, + ) assert mqtt_mock.async_publish.call_count == call_count # Any code numbered provided, should publish @@ -441,19 +518,50 @@ async def test_publish_mqtt_with_remote_code( @pytest.mark.parametrize( - ("hass_config", "service", "payload"), + ("hass_config", "service", "payload", "raises"), [ - (DEFAULT_CONFIG_REMOTE_CODE_TEXT, SERVICE_ALARM_ARM_HOME, "ARM_HOME"), - (DEFAULT_CONFIG_REMOTE_CODE_TEXT, SERVICE_ALARM_ARM_AWAY, "ARM_AWAY"), - (DEFAULT_CONFIG_REMOTE_CODE_TEXT, SERVICE_ALARM_ARM_NIGHT, "ARM_NIGHT"), - (DEFAULT_CONFIG_REMOTE_CODE_TEXT, SERVICE_ALARM_ARM_VACATION, "ARM_VACATION"), + ( + DEFAULT_CONFIG_REMOTE_CODE_TEXT, + SERVICE_ALARM_ARM_HOME, + "ARM_HOME", + pytest.raises(ServiceValidationError), + ), + ( + DEFAULT_CONFIG_REMOTE_CODE_TEXT, + SERVICE_ALARM_ARM_AWAY, + "ARM_AWAY", + pytest.raises(ServiceValidationError), + ), + ( + DEFAULT_CONFIG_REMOTE_CODE_TEXT, + SERVICE_ALARM_ARM_NIGHT, + "ARM_NIGHT", + pytest.raises(ServiceValidationError), + ), + ( + DEFAULT_CONFIG_REMOTE_CODE_TEXT, + SERVICE_ALARM_ARM_VACATION, + "ARM_VACATION", + pytest.raises(ServiceValidationError), + ), ( DEFAULT_CONFIG_REMOTE_CODE_TEXT, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "ARM_CUSTOM_BYPASS", + pytest.raises(ServiceValidationError), + ), + ( + DEFAULT_CONFIG_REMOTE_CODE_TEXT, + SERVICE_ALARM_DISARM, + "DISARM", + does_not_raise(), + ), + ( + DEFAULT_CONFIG_REMOTE_CODE_TEXT, + SERVICE_ALARM_TRIGGER, + "TRIGGER", + does_not_raise(), ), - (DEFAULT_CONFIG_REMOTE_CODE_TEXT, SERVICE_ALARM_DISARM, "DISARM"), - (DEFAULT_CONFIG_REMOTE_CODE_TEXT, SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) async def test_publish_mqtt_with_remote_code_text( @@ -461,18 +569,20 @@ async def test_publish_mqtt_with_remote_code_text( mqtt_mock_entry: MqttMockHAClientGenerator, service: str, payload: str, + raises: AbstractContextManager, ) -> None: """Test publishing of MQTT messages when remote text code is configured.""" mqtt_mock = await mqtt_mock_entry() call_count = mqtt_mock.async_publish.call_count # No code provided, should not publish - await hass.services.async_call( - alarm_control_panel.DOMAIN, - service, - {ATTR_ENTITY_ID: "alarm_control_panel.test"}, - blocking=True, - ) + with raises: + await hass.services.async_call( + alarm_control_panel.DOMAIN, + service, + {ATTR_ENTITY_ID: "alarm_control_panel.test"}, + blocking=True, + ) assert mqtt_mock.async_publish.call_count == call_count # Any code numbered provided, should publish diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index a6abff5b389..a24650c678c 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -154,7 +154,10 @@ async def test_optimistic_states(hass: HomeAssistant, start_ha) -> None: ("alarm_trigger", STATE_ALARM_TRIGGERED), ]: await hass.services.async_call( - ALARM_DOMAIN, service, {"entity_id": TEMPLATE_NAME}, blocking=True + ALARM_DOMAIN, + service, + {"entity_id": TEMPLATE_NAME, "code": "1234"}, + blocking=True, ) await hass.async_block_till_done() assert hass.states.get(TEMPLATE_NAME).state == set_state @@ -286,7 +289,10 @@ async def test_actions( ) -> None: """Test alarm actions.""" await hass.services.async_call( - ALARM_DOMAIN, service, {"entity_id": TEMPLATE_NAME}, blocking=True + ALARM_DOMAIN, + service, + {"entity_id": TEMPLATE_NAME, "code": "1234"}, + blocking=True, ) await hass.async_block_till_done() assert len(call_service_events) == 1 diff --git a/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr index 8261cd74859..0b8b8bb79ac 100644 --- a/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr +++ b/tests/components/totalconnect/snapshots/test_alarm_control_panel.ambr @@ -37,7 +37,7 @@ 'attributes': ReadOnlyDict({ 'ac_loss': False, 'changed_by': None, - 'code_arm_required': True, + 'code_arm_required': False, 'code_format': None, 'cover_tampered': False, 'friendly_name': 'test', @@ -95,7 +95,7 @@ 'attributes': ReadOnlyDict({ 'ac_loss': False, 'changed_by': None, - 'code_arm_required': True, + 'code_arm_required': False, 'code_format': None, 'cover_tampered': False, 'friendly_name': 'test Partition 2',