Add default code to alarm_control_panel (#112540)

This commit is contained in:
G Johansson 2024-05-29 10:46:53 +02:00 committed by GitHub
parent 38da61a5ac
commit 6b7ff2bf44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 680 additions and 74 deletions

View file

@ -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

View file

@ -53,6 +53,7 @@ class CanaryAlarm(
| AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.ARM_NIGHT
)
_attr_code_arm_required = False
def __init__(
self, coordinator: CanaryDataUpdateCoordinator, location: Location

View file

@ -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"

View file

@ -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:

View file

@ -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."""

View file

@ -74,6 +74,7 @@ class TotalConnectAlarm(TotalConnectLocationEntity, AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.ARM_NIGHT
)
_attr_code_arm_required = False
def __init__(
self,

View file

@ -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

View file

@ -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")

View file

@ -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(

View file

@ -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,
)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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',