Add modern configuration for template alarm control panel (#144834)

* Add modern configuration for template alarm control panel

* address comments and add tests for coverage

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Petro31 2025-05-15 05:46:57 -04:00 committed by GitHub
parent fa3edb5c01
commit 66ecc4d69d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 557 additions and 226 deletions

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from enum import Enum from enum import Enum
import logging import logging
from typing import Any from typing import TYPE_CHECKING
import voluptuous as vol import voluptuous as vol
@ -21,6 +21,7 @@ from homeassistant.const import (
ATTR_CODE, ATTR_CODE,
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_NAME, CONF_NAME,
CONF_STATE,
CONF_UNIQUE_ID, CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
@ -28,7 +29,7 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv, selector from homeassistant.helpers import config_validation as cv, selector, template
from homeassistant.helpers.device import async_device_info_to_link_from_device_id from homeassistant.helpers.device import async_device_info_to_link_from_device_id
from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.entity_platform import ( from homeassistant.helpers.entity_platform import (
@ -37,10 +38,15 @@ from homeassistant.helpers.entity_platform import (
) )
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify
from .const import DOMAIN from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN
from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf from .template_entity import (
LEGACY_FIELDS as TEMPLATE_ENTITY_LEGACY_FIELDS,
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
TEMPLATE_ENTITY_ICON_SCHEMA,
TemplateEntity,
rewrite_common_legacy_to_modern_conf,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_VALID_STATES = [ _VALID_STATES = [
@ -51,21 +57,22 @@ _VALID_STATES = [
AlarmControlPanelState.ARMED_VACATION, AlarmControlPanelState.ARMED_VACATION,
AlarmControlPanelState.ARMING, AlarmControlPanelState.ARMING,
AlarmControlPanelState.DISARMED, AlarmControlPanelState.DISARMED,
AlarmControlPanelState.DISARMING,
AlarmControlPanelState.PENDING, AlarmControlPanelState.PENDING,
AlarmControlPanelState.TRIGGERED, AlarmControlPanelState.TRIGGERED,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
] ]
CONF_ALARM_CONTROL_PANELS = "panels"
CONF_ARM_AWAY_ACTION = "arm_away" CONF_ARM_AWAY_ACTION = "arm_away"
CONF_ARM_CUSTOM_BYPASS_ACTION = "arm_custom_bypass" CONF_ARM_CUSTOM_BYPASS_ACTION = "arm_custom_bypass"
CONF_ARM_HOME_ACTION = "arm_home" CONF_ARM_HOME_ACTION = "arm_home"
CONF_ARM_NIGHT_ACTION = "arm_night" CONF_ARM_NIGHT_ACTION = "arm_night"
CONF_ARM_VACATION_ACTION = "arm_vacation" CONF_ARM_VACATION_ACTION = "arm_vacation"
CONF_DISARM_ACTION = "disarm"
CONF_TRIGGER_ACTION = "trigger"
CONF_ALARM_CONTROL_PANELS = "panels"
CONF_CODE_ARM_REQUIRED = "code_arm_required" CONF_CODE_ARM_REQUIRED = "code_arm_required"
CONF_CODE_FORMAT = "code_format" CONF_CODE_FORMAT = "code_format"
CONF_DISARM_ACTION = "disarm"
CONF_TRIGGER_ACTION = "trigger"
class TemplateCodeFormat(Enum): class TemplateCodeFormat(Enum):
@ -76,73 +83,140 @@ class TemplateCodeFormat(Enum):
text = CodeFormat.TEXT text = CodeFormat.TEXT
ALARM_CONTROL_PANEL_SCHEMA = vol.Schema( LEGACY_FIELDS = TEMPLATE_ENTITY_LEGACY_FIELDS | {
CONF_VALUE_TEMPLATE: CONF_STATE,
}
DEFAULT_NAME = "Template Alarm Control Panel"
ALARM_CONTROL_PANEL_SCHEMA = vol.All(
vol.Schema(
{
vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
vol.Optional(
CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name
): cv.enum(TemplateCodeFormat),
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_NAME): cv.template,
vol.Optional(CONF_PICTURE): cv.template,
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)
.extend(TEMPLATE_ENTITY_AVAILABILITY_SCHEMA.schema)
.extend(TEMPLATE_ENTITY_ICON_SCHEMA.schema),
)
LEGACY_ALARM_CONTROL_PANEL_SCHEMA = vol.Schema(
{ {
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum( vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum(
TemplateCodeFormat TemplateCodeFormat
), ),
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
} }
) )
PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend(
{ {
vol.Required(CONF_ALARM_CONTROL_PANELS): cv.schema_with_slug_keys( vol.Required(CONF_ALARM_CONTROL_PANELS): cv.schema_with_slug_keys(
ALARM_CONTROL_PANEL_SCHEMA LEGACY_ALARM_CONTROL_PANEL_SCHEMA
), ),
} }
) )
ALARM_CONTROL_PANEL_CONFIG_SCHEMA = vol.Schema( ALARM_CONTROL_PANEL_CONFIG_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_NAME): cv.template,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum( vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum(
TemplateCodeFormat TemplateCodeFormat
), ),
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(), vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_NAME): cv.template,
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
} }
) )
async def _async_create_entities( def rewrite_legacy_to_modern_conf(
hass: HomeAssistant, config: dict[str, Any] hass: HomeAssistant, config: dict[str, dict]
) -> list[AlarmControlPanelTemplate]: ) -> list[dict]:
"""Create Template Alarm Control Panels.""" """Rewrite legacy alarm control panel configuration definitions to modern ones."""
alarm_control_panels = [] alarm_control_panels = []
for object_id, entity_config in config[CONF_ALARM_CONTROL_PANELS].items(): for object_id, entity_conf in config.items():
entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config) entity_conf = {**entity_conf, CONF_OBJECT_ID: object_id}
unique_id = entity_config.get(CONF_UNIQUE_ID)
entity_conf = rewrite_common_legacy_to_modern_conf(
hass, entity_conf, LEGACY_FIELDS
)
if CONF_NAME not in entity_conf:
entity_conf[CONF_NAME] = template.Template(object_id, hass)
alarm_control_panels.append(entity_conf)
return alarm_control_panels
@callback
def _async_create_template_tracking_entities(
async_add_entities: AddEntitiesCallback,
hass: HomeAssistant,
definitions: list[dict],
unique_id_prefix: str | None,
) -> None:
"""Create the template alarm control panels."""
alarm_control_panels = []
for entity_conf in definitions:
unique_id = entity_conf.get(CONF_UNIQUE_ID)
if unique_id and unique_id_prefix:
unique_id = f"{unique_id_prefix}-{unique_id}"
alarm_control_panels.append( alarm_control_panels.append(
AlarmControlPanelTemplate( AlarmControlPanelTemplate(
hass, hass,
object_id, entity_conf,
entity_config,
unique_id, unique_id,
) )
) )
return alarm_control_panels async_add_entities(alarm_control_panels)
def rewrite_options_to_modern_conf(option_config: dict[str, dict]) -> dict[str, dict]:
"""Rewrite option configuration to modern configuration."""
option_config = {**option_config}
if CONF_VALUE_TEMPLATE in option_config:
option_config[CONF_STATE] = option_config.pop(CONF_VALUE_TEMPLATE)
return option_config
async def async_setup_entry( async def async_setup_entry(
@ -153,12 +227,12 @@ async def async_setup_entry(
"""Initialize config entry.""" """Initialize config entry."""
_options = dict(config_entry.options) _options = dict(config_entry.options)
_options.pop("template_type") _options.pop("template_type")
_options = rewrite_options_to_modern_conf(_options)
validated_config = ALARM_CONTROL_PANEL_CONFIG_SCHEMA(_options) validated_config = ALARM_CONTROL_PANEL_CONFIG_SCHEMA(_options)
async_add_entities( async_add_entities(
[ [
AlarmControlPanelTemplate( AlarmControlPanelTemplate(
hass, hass,
slugify(_options[CONF_NAME]),
validated_config, validated_config,
config_entry.entry_id, config_entry.entry_id,
) )
@ -172,8 +246,22 @@ async def async_setup_platform(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> None: ) -> None:
"""Set up the Template Alarm Control Panels.""" """Set up the Template cover."""
async_add_entities(await _async_create_entities(hass, config)) if discovery_info is None:
_async_create_template_tracking_entities(
async_add_entities,
hass,
rewrite_legacy_to_modern_conf(hass, config[CONF_ALARM_CONTROL_PANELS]),
None,
)
return
_async_create_template_tracking_entities(
async_add_entities,
hass,
discovery_info["entities"],
discovery_info["unique_id"],
)
class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, RestoreEntity): class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, RestoreEntity):
@ -184,20 +272,20 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
object_id: str,
config: dict, config: dict,
unique_id: str | None, unique_id: str | None,
) -> None: ) -> None:
"""Initialize the panel.""" """Initialize the panel."""
super().__init__( super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id)
hass, config=config, fallback_name=object_id, unique_id=unique_id if (object_id := config.get(CONF_OBJECT_ID)) is not None:
) self.entity_id = async_generate_entity_id(
self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass
ENTITY_ID_FORMAT, object_id, hass=hass )
)
name = self._attr_name name = self._attr_name
assert name is not None if TYPE_CHECKING:
self._template = config.get(CONF_VALUE_TEMPLATE) assert name is not None
self._template = config.get(CONF_STATE)
self._attr_code_arm_required: bool = config[CONF_CODE_ARM_REQUIRED] self._attr_code_arm_required: bool = config[CONF_CODE_ARM_REQUIRED]
self._attr_code_format = config[CONF_CODE_FORMAT].value self._attr_code_format = config[CONF_CODE_FORMAT].value

View File

@ -7,6 +7,9 @@ from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant.components.alarm_control_panel import (
DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
)
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.blueprint import ( from homeassistant.components.blueprint import (
is_blueprint_instance_config, is_blueprint_instance_config,
@ -45,6 +48,7 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import async_notify_setup_error from homeassistant.setup import async_notify_setup_error
from . import ( from . import (
alarm_control_panel as alarm_control_panel_platform,
binary_sensor as binary_sensor_platform, binary_sensor as binary_sensor_platform,
button as button_platform, button as button_platform,
cover as cover_platform, cover as cover_platform,
@ -114,6 +118,10 @@ CONFIG_SECTION_SCHEMA = vol.All(
vol.Optional(CONF_BINARY_SENSORS): cv.schema_with_slug_keys( vol.Optional(CONF_BINARY_SENSORS): cv.schema_with_slug_keys(
binary_sensor_platform.LEGACY_BINARY_SENSOR_SCHEMA binary_sensor_platform.LEGACY_BINARY_SENSOR_SCHEMA
), ),
vol.Optional(ALARM_CONTROL_PANEL_DOMAIN): vol.All(
cv.ensure_list,
[alarm_control_panel_platform.ALARM_CONTROL_PANEL_SCHEMA],
),
vol.Optional(SELECT_DOMAIN): vol.All( vol.Optional(SELECT_DOMAIN): vol.All(
cv.ensure_list, [select_platform.SELECT_SCHEMA] cv.ensure_list, [select_platform.SELECT_SCHEMA]
), ),
@ -144,7 +152,7 @@ CONFIG_SECTION_SCHEMA = vol.All(
}, },
), ),
ensure_domains_do_not_have_trigger_or_action( ensure_domains_do_not_have_trigger_or_action(
BUTTON_DOMAIN, COVER_DOMAIN, FAN_DOMAIN, LOCK_DOMAIN ALARM_CONTROL_PANEL_DOMAIN, BUTTON_DOMAIN, COVER_DOMAIN, FAN_DOMAIN, LOCK_DOMAIN
), ),
) )

View File

@ -1,5 +1,7 @@
"""The tests for the Template alarm control panel platform.""" """The tests for the Template alarm control panel platform."""
from typing import Any
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
@ -13,6 +15,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
ATTR_SERVICE_DATA, ATTR_SERVICE_DATA,
EVENT_CALL_SERVICE, EVENT_CALL_SERVICE,
STATE_ON,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
) )
@ -20,10 +23,13 @@ from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .conftest import ConfigurationStyle
from tests.common import MockConfigEntry, assert_setup_component, mock_restore_cache from tests.common import MockConfigEntry, assert_setup_component, mock_restore_cache
TEMPLATE_NAME = "alarm_control_panel.test_template_panel" TEST_OBJECT_ID = "test_template_panel"
PANEL_NAME = "alarm_control_panel.test" TEST_ENTITY_ID = f"alarm_control_panel.{TEST_OBJECT_ID}"
TEST_STATE_ENTITY_ID = "alarm_control_panel.test"
@pytest.fixture @pytest.fixture
@ -93,50 +99,295 @@ EMPTY_ACTIONS = {
} }
UNIQUE_ID_CONFIG = {
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
"unique_id": "not-so-unique-anymore",
}
TEMPLATE_ALARM_CONFIG = { TEMPLATE_ALARM_CONFIG = {
"value_template": "{{ states('alarm_control_panel.test') }}", "value_template": "{{ states('alarm_control_panel.test') }}",
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG, **OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
} }
@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")]) async def async_setup_legacy_format(
hass: HomeAssistant, count: int, panel_config: dict[str, Any]
) -> None:
"""Do setup of alarm control panel integration via legacy format."""
config = {"alarm_control_panel": {"platform": "template", "panels": panel_config}}
with assert_setup_component(count, ALARM_DOMAIN):
assert await async_setup_component(
hass,
ALARM_DOMAIN,
config,
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
async def async_setup_modern_format(
hass: HomeAssistant, count: int, panel_config: dict[str, Any]
) -> None:
"""Do setup of alarm control panel integration via modern format."""
config = {"template": {"alarm_control_panel": panel_config}}
with assert_setup_component(count, template.DOMAIN):
assert await async_setup_component(
hass,
template.DOMAIN,
config,
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
@pytest.fixture
async def setup_panel(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
panel_config: dict[str, Any],
) -> None:
"""Do setup of alarm control panel integration."""
if style == ConfigurationStyle.LEGACY:
await async_setup_legacy_format(hass, count, panel_config)
elif style == ConfigurationStyle.MODERN:
await async_setup_modern_format(hass, count, panel_config)
async def async_setup_state_panel(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
state_template: str,
):
"""Do setup of alarm control panel integration using a state template."""
if style == ConfigurationStyle.LEGACY:
await async_setup_legacy_format(
hass,
count,
{
TEST_OBJECT_ID: {
"value_template": state_template,
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
}
},
)
elif style == ConfigurationStyle.MODERN:
await async_setup_modern_format(
hass,
count,
{
"name": TEST_OBJECT_ID,
"state": state_template,
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
},
)
@pytest.fixture
async def setup_state_panel(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
state_template: str,
):
"""Do setup of alarm control panel integration using a state template."""
await async_setup_state_panel(hass, count, style, state_template)
@pytest.fixture
async def setup_base_panel(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
state_template: str | None,
panel_config: str,
):
"""Do setup of alarm control panel integration using a state template."""
if style == ConfigurationStyle.LEGACY:
extra = {"value_template": state_template} if state_template else {}
await async_setup_legacy_format(
hass,
count,
{TEST_OBJECT_ID: {**extra, **panel_config}},
)
elif style == ConfigurationStyle.MODERN:
extra = {"state": state_template} if state_template else {}
await async_setup_modern_format(
hass,
count,
{
"name": TEST_OBJECT_ID,
**extra,
**panel_config,
},
)
@pytest.fixture
async def setup_single_attribute_state_panel(
hass: HomeAssistant,
count: int,
style: ConfigurationStyle,
state_template: str,
attribute: str,
attribute_template: str,
) -> None:
"""Do setup of alarm control panel integration testing a single attribute."""
extra = {attribute: attribute_template} if attribute and attribute_template else {}
if style == ConfigurationStyle.LEGACY:
await async_setup_legacy_format(
hass,
count,
{
TEST_OBJECT_ID: {
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
"value_template": state_template,
**extra,
}
},
)
elif style == ConfigurationStyle.MODERN:
await async_setup_modern_format(
hass,
count,
{
"name": TEST_OBJECT_ID,
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
"state": state_template,
**extra,
},
)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config", ("count", "state_template"), [(1, "{{ states('alarm_control_panel.test') }}")]
[
{
"alarm_control_panel": {
"platform": "template",
"panels": {"test_template_panel": TEMPLATE_ALARM_CONFIG},
}
},
],
) )
@pytest.mark.usefixtures("start_ha") @pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
)
@pytest.mark.usefixtures("setup_state_panel")
async def test_template_state_text(hass: HomeAssistant) -> None: async def test_template_state_text(hass: HomeAssistant) -> None:
"""Test the state text of a template.""" """Test the state text of a template."""
for set_state in ( for set_state in (
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelState.ARMED_AWAY, AlarmControlPanelState.ARMED_AWAY,
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelState.ARMED_NIGHT, AlarmControlPanelState.ARMED_NIGHT,
AlarmControlPanelState.ARMED_VACATION, AlarmControlPanelState.ARMED_VACATION,
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
AlarmControlPanelState.ARMING, AlarmControlPanelState.ARMING,
AlarmControlPanelState.DISARMED, AlarmControlPanelState.DISARMED,
AlarmControlPanelState.DISARMING,
AlarmControlPanelState.PENDING, AlarmControlPanelState.PENDING,
AlarmControlPanelState.TRIGGERED, AlarmControlPanelState.TRIGGERED,
): ):
hass.states.async_set(PANEL_NAME, set_state) hass.states.async_set(TEST_STATE_ENTITY_ID, set_state)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(TEMPLATE_NAME) state = hass.states.get(TEST_ENTITY_ID)
assert state.state == set_state assert state.state == set_state
hass.states.async_set(PANEL_NAME, "invalid_state") hass.states.async_set(TEST_STATE_ENTITY_ID, "invalid_state")
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(TEMPLATE_NAME) state = hass.states.get(TEST_ENTITY_ID)
assert state.state == "unknown" assert state.state == "unknown"
@pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize(
("state_template", "expected"),
[
("{{ 'disarmed' }}", AlarmControlPanelState.DISARMED),
("{{ 'armed_home' }}", AlarmControlPanelState.ARMED_HOME),
("{{ 'armed_away' }}", AlarmControlPanelState.ARMED_AWAY),
("{{ 'armed_night' }}", AlarmControlPanelState.ARMED_NIGHT),
("{{ 'armed_vacation' }}", AlarmControlPanelState.ARMED_VACATION),
("{{ 'armed_custom_bypass' }}", AlarmControlPanelState.ARMED_CUSTOM_BYPASS),
("{{ 'pending' }}", AlarmControlPanelState.PENDING),
("{{ 'arming' }}", AlarmControlPanelState.ARMING),
("{{ 'disarming' }}", AlarmControlPanelState.DISARMING),
("{{ 'triggered' }}", AlarmControlPanelState.TRIGGERED),
("{{ x - 1 }}", STATE_UNKNOWN),
],
)
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
)
@pytest.mark.usefixtures("setup_state_panel")
async def test_state_template_states(hass: HomeAssistant, expected: str) -> None:
"""Test the state template."""
state = hass.states.get(TEST_ENTITY_ID)
assert state.state == expected
@pytest.mark.parametrize(
("count", "state_template", "attribute_template"),
[
(
1,
"{{ 'disarmed' }}",
"{% if states.switch.test_state.state %}mdi:check{% endif %}",
)
],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.MODERN, "icon"),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_panel")
async def test_icon_template(
hass: HomeAssistant,
) -> None:
"""Test icon template."""
state = hass.states.get(TEST_ENTITY_ID)
assert state.attributes.get("icon") in ("", None)
hass.states.async_set("switch.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY_ID)
assert state.attributes["icon"] == "mdi:check"
@pytest.mark.parametrize(
("count", "state_template", "attribute_template"),
[
(
1,
"{{ 'disarmed' }}",
"{% if states.switch.test_state.state %}local/panel.png{% endif %}",
)
],
)
@pytest.mark.parametrize(
("style", "attribute"),
[
(ConfigurationStyle.MODERN, "picture"),
],
)
@pytest.mark.usefixtures("setup_single_attribute_state_panel")
async def test_picture_template(
hass: HomeAssistant,
) -> None:
"""Test icon template."""
state = hass.states.get(TEST_ENTITY_ID)
assert state.attributes.get("entity_picture") in ("", None)
hass.states.async_set("switch.test_state", STATE_ON)
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY_ID)
assert state.attributes["entity_picture"] == "local/panel.png"
async def test_setup_config_entry( async def test_setup_config_entry(
hass: HomeAssistant, snapshot: SnapshotAssertion hass: HomeAssistant, snapshot: SnapshotAssertion
) -> None: ) -> None:
@ -172,29 +423,18 @@ async def test_setup_config_entry(
assert state.state == AlarmControlPanelState.DISARMED assert state.state == AlarmControlPanelState.DISARMED
@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")]) @pytest.mark.parametrize(("count", "state_template"), [(1, None)])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config", "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
[
{
"alarm_control_panel": {
"platform": "template",
"panels": {"test_template_panel": OPTIMISTIC_TEMPLATE_ALARM_CONFIG},
}
},
{
"alarm_control_panel": {
"platform": "template",
"panels": {"test_template_panel": EMPTY_ACTIONS},
}
},
],
) )
@pytest.mark.usefixtures("start_ha") @pytest.mark.parametrize(
"panel_config", [OPTIMISTIC_TEMPLATE_ALARM_CONFIG, EMPTY_ACTIONS]
)
@pytest.mark.usefixtures("setup_base_panel")
async def test_optimistic_states(hass: HomeAssistant) -> None: async def test_optimistic_states(hass: HomeAssistant) -> None:
"""Test the optimistic state.""" """Test the optimistic state."""
state = hass.states.get(TEMPLATE_NAME) state = hass.states.get(TEST_ENTITY_ID)
await hass.async_block_till_done() await hass.async_block_till_done()
assert state.state == "unknown" assert state.state == "unknown"
@ -210,31 +450,45 @@ async def test_optimistic_states(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
ALARM_DOMAIN, ALARM_DOMAIN,
service, service,
{"entity_id": TEMPLATE_NAME, "code": "1234"}, {"entity_id": TEST_ENTITY_ID, "code": "1234"},
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get(TEMPLATE_NAME).state == set_state assert hass.states.get(TEST_ENTITY_ID).state == set_state
@pytest.mark.parametrize("count", [0])
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
)
@pytest.mark.parametrize(
("panel_config", "state_template", "msg"),
[
(
OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
"{% if blah %}",
"invalid template",
),
(
{"code_format": "bad_format", **OPTIMISTIC_TEMPLATE_ALARM_CONFIG},
"disarmed",
"value must be one of ['no_code', 'number', 'text']",
),
],
)
@pytest.mark.usefixtures("setup_base_panel")
async def test_template_syntax_error(
hass: HomeAssistant, msg, caplog_setup_text
) -> None:
"""Test templating syntax error."""
assert len(hass.states.async_all("alarm_control_panel")) == 0
assert (msg) in caplog_setup_text
@pytest.mark.parametrize(("count", "domain"), [(0, "alarm_control_panel")]) @pytest.mark.parametrize(("count", "domain"), [(0, "alarm_control_panel")])
@pytest.mark.parametrize( @pytest.mark.parametrize(
("config", "msg"), ("config", "msg"),
[ [
(
{
"alarm_control_panel": {
"platform": "template",
"panels": {
"test_template_panel": {
"value_template": "{% if blah %}",
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
}
},
}
},
"invalid template",
),
( (
{ {
"alarm_control_panel": { "alarm_control_panel": {
@ -264,25 +518,10 @@ async def test_optimistic_states(hass: HomeAssistant) -> None:
}, },
"required key 'panels' not provided", "required key 'panels' not provided",
), ),
(
{
"alarm_control_panel": {
"platform": "template",
"panels": {
"test_template_panel": {
"value_template": "disarmed",
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
"code_format": "bad_format",
}
},
}
},
"value must be one of ['no_code', 'number', 'text']",
),
], ],
) )
@pytest.mark.usefixtures("start_ha") @pytest.mark.usefixtures("start_ha")
async def test_template_syntax_error( async def test_legacy_template_syntax_error(
hass: HomeAssistant, msg, caplog_setup_text hass: HomeAssistant, msg, caplog_setup_text
) -> None: ) -> None:
"""Test templating syntax error.""" """Test templating syntax error."""
@ -290,43 +529,30 @@ async def test_template_syntax_error(
assert (msg) in caplog_setup_text assert (msg) in caplog_setup_text
@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config", ("count", "state_template", "attribute", "attribute_template"),
[(1, "disarmed", "name", '{{ "Template Alarm Panel" }}')],
)
@pytest.mark.parametrize(
("style", "test_entity_id"),
[ [
{ (ConfigurationStyle.LEGACY, TEST_ENTITY_ID),
"alarm_control_panel": { (ConfigurationStyle.MODERN, "alarm_control_panel.template_alarm_panel"),
"platform": "template",
"panels": {
"test_template_panel": {
"name": '{{ "Template Alarm Panel" }}',
"value_template": "disarmed",
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
}
},
}
},
], ],
) )
@pytest.mark.usefixtures("start_ha") @pytest.mark.usefixtures("setup_single_attribute_state_panel")
async def test_name(hass: HomeAssistant) -> None: async def test_name(hass: HomeAssistant, test_entity_id: str) -> None:
"""Test the accessibility of the name attribute.""" """Test the accessibility of the name attribute."""
state = hass.states.get(TEMPLATE_NAME) state = hass.states.get(test_entity_id)
assert state is not None assert state is not None
assert state.attributes.get("friendly_name") == "Template Alarm Panel" assert state.attributes.get("friendly_name") == "Template Alarm Panel"
@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config", ("count", "state_template"), [(1, "{{ states('alarm_control_panel.test') }}")]
[ )
{ @pytest.mark.parametrize(
"alarm_control_panel": { "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
"platform": "template",
"panels": {"test_template_panel": TEMPLATE_ALARM_CONFIG},
}
},
],
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
"service", "service",
@ -340,7 +566,7 @@ async def test_name(hass: HomeAssistant) -> None:
"alarm_trigger", "alarm_trigger",
], ],
) )
@pytest.mark.usefixtures("start_ha") @pytest.mark.usefixtures("setup_state_panel")
async def test_actions( async def test_actions(
hass: HomeAssistant, service, call_service_events: list[Event] hass: HomeAssistant, service, call_service_events: list[Event]
) -> None: ) -> None:
@ -348,128 +574,147 @@ async def test_actions(
await hass.services.async_call( await hass.services.async_call(
ALARM_DOMAIN, ALARM_DOMAIN,
service, service,
{"entity_id": TEMPLATE_NAME, "code": "1234"}, {"entity_id": TEST_ENTITY_ID, "code": "1234"},
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(call_service_events) == 1 assert len(call_service_events) == 1
assert call_service_events[0].data["service"] == service assert call_service_events[0].data["service"] == service
assert call_service_events[0].data["service_data"]["code"] == TEMPLATE_NAME assert call_service_events[0].data["service_data"]["code"] == TEST_ENTITY_ID
@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")]) @pytest.mark.parametrize("count", [1])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config", ("panel_config", "style"),
[ [
{ (
"alarm_control_panel": { {
"platform": "template", "test_template_alarm_control_panel_01": {
"panels": { "value_template": "{{ true }}",
"test_template_alarm_control_panel_01": { **UNIQUE_ID_CONFIG,
"unique_id": "not-so-unique-anymore", },
"value_template": "{{ true }}", "test_template_alarm_control_panel_02": {
}, "value_template": "{{ false }}",
"test_template_alarm_control_panel_02": { **UNIQUE_ID_CONFIG,
"unique_id": "not-so-unique-anymore",
"value_template": "{{ false }}",
},
}, },
}, },
}, ConfigurationStyle.LEGACY,
),
(
[
{
"name": "test_template_alarm_control_panel_01",
"state": "{{ true }}",
**UNIQUE_ID_CONFIG,
},
{
"name": "test_template_alarm_control_panel_02",
"state": "{{ false }}",
**UNIQUE_ID_CONFIG,
},
],
ConfigurationStyle.MODERN,
),
], ],
) )
@pytest.mark.usefixtures("start_ha") @pytest.mark.usefixtures("setup_panel")
async def test_unique_id(hass: HomeAssistant) -> None: async def test_unique_id(hass: HomeAssistant) -> None:
"""Test unique_id option only creates one alarm control panel per id.""" """Test unique_id option only creates one alarm control panel per id."""
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 1
@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")]) async def test_nested_unique_id(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test a template unique_id propagates to alarm_control_panel unique_ids."""
with assert_setup_component(1, template.DOMAIN):
assert await async_setup_component(
hass,
template.DOMAIN,
{
"template": {
"unique_id": "x",
"alarm_control_panel": [
{
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
"name": "test_a",
"unique_id": "a",
"state": "{{ true }}",
},
{
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
"name": "test_b",
"unique_id": "b",
"state": "{{ true }}",
},
],
},
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
assert len(hass.states.async_all("alarm_control_panel")) == 2
entry = entity_registry.async_get("alarm_control_panel.test_a")
assert entry
assert entry.unique_id == "x-a"
entry = entity_registry.async_get("alarm_control_panel.test_b")
assert entry
assert entry.unique_id == "x-b"
@pytest.mark.parametrize(("count", "state_template"), [(1, "disarmed")])
@pytest.mark.parametrize( @pytest.mark.parametrize(
("config", "code_format", "code_arm_required"), "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
)
@pytest.mark.parametrize(
("panel_config", "code_format", "code_arm_required"),
[ [
( (
{ {},
"alarm_control_panel": {
"platform": "template",
"panels": {
"test_template_panel": {
"value_template": "disarmed",
}
},
}
},
"number", "number",
True, True,
), ),
( (
{ {"code_format": "text"},
"alarm_control_panel": {
"platform": "template",
"panels": {
"test_template_panel": {
"value_template": "disarmed",
"code_format": "text",
}
},
}
},
"text", "text",
True, True,
), ),
( (
{ {
"alarm_control_panel": { "code_format": "no_code",
"platform": "template", "code_arm_required": False,
"panels": {
"test_template_panel": {
"value_template": "disarmed",
"code_format": "no_code",
"code_arm_required": False,
}
},
}
}, },
None, None,
False, False,
), ),
( (
{ {
"alarm_control_panel": { "code_format": "text",
"platform": "template", "code_arm_required": False,
"panels": {
"test_template_panel": {
"value_template": "disarmed",
"code_format": "text",
"code_arm_required": False,
}
},
}
}, },
"text", "text",
False, False,
), ),
], ],
) )
@pytest.mark.usefixtures("start_ha") @pytest.mark.usefixtures("setup_base_panel")
async def test_code_config(hass: HomeAssistant, code_format, code_arm_required) -> None: async def test_code_config(hass: HomeAssistant, code_format, code_arm_required) -> None:
"""Test configuration options related to alarm code.""" """Test configuration options related to alarm code."""
state = hass.states.get(TEMPLATE_NAME) state = hass.states.get(TEST_ENTITY_ID)
assert state.attributes.get("code_format") == code_format assert state.attributes.get("code_format") == code_format
assert state.attributes.get("code_arm_required") == code_arm_required assert state.attributes.get("code_arm_required") == code_arm_required
@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config", ("count", "state_template"), [(1, "{{ states('alarm_control_panel.test') }}")]
[ )
{ @pytest.mark.parametrize(
"alarm_control_panel": { "style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
"platform": "template",
"panels": {"test_template_panel": TEMPLATE_ALARM_CONFIG},
}
},
],
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
("restored_state", "initial_state"), ("restored_state", "initial_state"),
@ -508,11 +753,11 @@ async def test_code_config(hass: HomeAssistant, code_format, code_arm_required)
) )
async def test_restore_state( async def test_restore_state(
hass: HomeAssistant, hass: HomeAssistant,
count, count: int,
domain, state_template: str,
config, style: ConfigurationStyle,
restored_state, restored_state: str,
initial_state, initial_state: str,
) -> None: ) -> None:
"""Test restoring template alarm control panel.""" """Test restoring template alarm control panel."""
@ -522,17 +767,7 @@ async def test_restore_state(
{}, {},
) )
mock_restore_cache(hass, (fake_state,)) mock_restore_cache(hass, (fake_state,))
with assert_setup_component(count, domain): await async_setup_state_panel(hass, count, style, state_template)
assert await async_setup_component(
hass,
domain,
config,
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
state = hass.states.get("alarm_control_panel.test_template_panel") state = hass.states.get("alarm_control_panel.test_template_panel")
assert state.state == initial_state assert state.state == initial_state