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
import logging
from typing import Any
from typing import TYPE_CHECKING
import voluptuous as vol
@ -21,6 +21,7 @@ from homeassistant.const import (
ATTR_CODE,
CONF_DEVICE_ID,
CONF_NAME,
CONF_STATE,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
STATE_UNAVAILABLE,
@ -28,7 +29,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
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.entity import async_generate_entity_id
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.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import slugify
from .const import DOMAIN
from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf
from .const import CONF_OBJECT_ID, CONF_PICTURE, DOMAIN
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__)
_VALID_STATES = [
@ -51,21 +57,22 @@ _VALID_STATES = [
AlarmControlPanelState.ARMED_VACATION,
AlarmControlPanelState.ARMING,
AlarmControlPanelState.DISARMED,
AlarmControlPanelState.DISARMING,
AlarmControlPanelState.PENDING,
AlarmControlPanelState.TRIGGERED,
STATE_UNAVAILABLE,
]
CONF_ALARM_CONTROL_PANELS = "panels"
CONF_ARM_AWAY_ACTION = "arm_away"
CONF_ARM_CUSTOM_BYPASS_ACTION = "arm_custom_bypass"
CONF_ARM_HOME_ACTION = "arm_home"
CONF_ARM_NIGHT_ACTION = "arm_night"
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_FORMAT = "code_format"
CONF_DISARM_ACTION = "disarm"
CONF_TRIGGER_ACTION = "trigger"
class TemplateCodeFormat(Enum):
@ -76,73 +83,140 @@ class TemplateCodeFormat(Enum):
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_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_TRIGGER_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.string,
vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
}
)
PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend(
{
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(
{
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_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_TRIGGER_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_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(
hass: HomeAssistant, config: dict[str, Any]
) -> list[AlarmControlPanelTemplate]:
"""Create Template Alarm Control Panels."""
def rewrite_legacy_to_modern_conf(
hass: HomeAssistant, config: dict[str, dict]
) -> list[dict]:
"""Rewrite legacy alarm control panel configuration definitions to modern ones."""
alarm_control_panels = []
for object_id, entity_config in config[CONF_ALARM_CONTROL_PANELS].items():
entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config)
unique_id = entity_config.get(CONF_UNIQUE_ID)
for object_id, entity_conf in config.items():
entity_conf = {**entity_conf, CONF_OBJECT_ID: object_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(
AlarmControlPanelTemplate(
hass,
object_id,
entity_config,
entity_conf,
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(
@ -153,12 +227,12 @@ async def async_setup_entry(
"""Initialize config entry."""
_options = dict(config_entry.options)
_options.pop("template_type")
_options = rewrite_options_to_modern_conf(_options)
validated_config = ALARM_CONTROL_PANEL_CONFIG_SCHEMA(_options)
async_add_entities(
[
AlarmControlPanelTemplate(
hass,
slugify(_options[CONF_NAME]),
validated_config,
config_entry.entry_id,
)
@ -172,8 +246,22 @@ async def async_setup_platform(
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Template Alarm Control Panels."""
async_add_entities(await _async_create_entities(hass, config))
"""Set up the Template cover."""
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):
@ -184,20 +272,20 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity, Restore
def __init__(
self,
hass: HomeAssistant,
object_id: str,
config: dict,
unique_id: str | None,
) -> None:
"""Initialize the panel."""
super().__init__(
hass, config=config, fallback_name=object_id, unique_id=unique_id
)
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, object_id, hass=hass
)
super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id)
if (object_id := config.get(CONF_OBJECT_ID)) is not None:
self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, object_id, hass=hass
)
name = self._attr_name
assert name is not None
self._template = config.get(CONF_VALUE_TEMPLATE)
if TYPE_CHECKING:
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_format = config[CONF_CODE_FORMAT].value

View File

@ -7,6 +7,9 @@ from typing import Any
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.blueprint import (
is_blueprint_instance_config,
@ -45,6 +48,7 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import async_notify_setup_error
from . import (
alarm_control_panel as alarm_control_panel_platform,
binary_sensor as binary_sensor_platform,
button as button_platform,
cover as cover_platform,
@ -114,6 +118,10 @@ CONFIG_SECTION_SCHEMA = vol.All(
vol.Optional(CONF_BINARY_SENSORS): cv.schema_with_slug_keys(
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(
cv.ensure_list, [select_platform.SELECT_SCHEMA]
),
@ -144,7 +152,7 @@ CONFIG_SECTION_SCHEMA = vol.All(
},
),
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."""
from typing import Any
import pytest
from syrupy.assertion import SnapshotAssertion
@ -13,6 +15,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SERVICE_DATA,
EVENT_CALL_SERVICE,
STATE_ON,
STATE_UNAVAILABLE,
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.setup import async_setup_component
from .conftest import ConfigurationStyle
from tests.common import MockConfigEntry, assert_setup_component, mock_restore_cache
TEMPLATE_NAME = "alarm_control_panel.test_template_panel"
PANEL_NAME = "alarm_control_panel.test"
TEST_OBJECT_ID = "test_template_panel"
TEST_ENTITY_ID = f"alarm_control_panel.{TEST_OBJECT_ID}"
TEST_STATE_ENTITY_ID = "alarm_control_panel.test"
@pytest.fixture
@ -93,50 +99,295 @@ EMPTY_ACTIONS = {
}
UNIQUE_ID_CONFIG = {
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
"unique_id": "not-so-unique-anymore",
}
TEMPLATE_ALARM_CONFIG = {
"value_template": "{{ states('alarm_control_panel.test') }}",
**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(
"config",
[
{
"alarm_control_panel": {
"platform": "template",
"panels": {"test_template_panel": TEMPLATE_ALARM_CONFIG},
}
},
],
("count", "state_template"), [(1, "{{ states('alarm_control_panel.test') }}")]
)
@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:
"""Test the state text of a template."""
for set_state in (
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelState.ARMED_AWAY,
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelState.ARMED_NIGHT,
AlarmControlPanelState.ARMED_VACATION,
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
AlarmControlPanelState.ARMING,
AlarmControlPanelState.DISARMED,
AlarmControlPanelState.DISARMING,
AlarmControlPanelState.PENDING,
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()
state = hass.states.get(TEMPLATE_NAME)
state = hass.states.get(TEST_ENTITY_ID)
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()
state = hass.states.get(TEMPLATE_NAME)
state = hass.states.get(TEST_ENTITY_ID)
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(
hass: HomeAssistant, snapshot: SnapshotAssertion
) -> None:
@ -172,29 +423,18 @@ async def test_setup_config_entry(
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(
"config",
[
{
"alarm_control_panel": {
"platform": "template",
"panels": {"test_template_panel": OPTIMISTIC_TEMPLATE_ALARM_CONFIG},
}
},
{
"alarm_control_panel": {
"platform": "template",
"panels": {"test_template_panel": EMPTY_ACTIONS},
}
},
],
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
)
@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:
"""Test the optimistic state."""
state = hass.states.get(TEMPLATE_NAME)
state = hass.states.get(TEST_ENTITY_ID)
await hass.async_block_till_done()
assert state.state == "unknown"
@ -210,31 +450,45 @@ async def test_optimistic_states(hass: HomeAssistant) -> None:
await hass.services.async_call(
ALARM_DOMAIN,
service,
{"entity_id": TEMPLATE_NAME, "code": "1234"},
{"entity_id": TEST_ENTITY_ID, "code": "1234"},
blocking=True,
)
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(
("config", "msg"),
[
(
{
"alarm_control_panel": {
"platform": "template",
"panels": {
"test_template_panel": {
"value_template": "{% if blah %}",
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
}
},
}
},
"invalid template",
),
(
{
"alarm_control_panel": {
@ -264,25 +518,10 @@ async def test_optimistic_states(hass: HomeAssistant) -> None:
},
"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")
async def test_template_syntax_error(
async def test_legacy_template_syntax_error(
hass: HomeAssistant, msg, caplog_setup_text
) -> None:
"""Test templating syntax error."""
@ -290,43 +529,30 @@ async def test_template_syntax_error(
assert (msg) in caplog_setup_text
@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")])
@pytest.mark.parametrize(
"config",
("count", "state_template", "attribute", "attribute_template"),
[(1, "disarmed", "name", '{{ "Template Alarm Panel" }}')],
)
@pytest.mark.parametrize(
("style", "test_entity_id"),
[
{
"alarm_control_panel": {
"platform": "template",
"panels": {
"test_template_panel": {
"name": '{{ "Template Alarm Panel" }}',
"value_template": "disarmed",
**OPTIMISTIC_TEMPLATE_ALARM_CONFIG,
}
},
}
},
(ConfigurationStyle.LEGACY, TEST_ENTITY_ID),
(ConfigurationStyle.MODERN, "alarm_control_panel.template_alarm_panel"),
],
)
@pytest.mark.usefixtures("start_ha")
async def test_name(hass: HomeAssistant) -> None:
@pytest.mark.usefixtures("setup_single_attribute_state_panel")
async def test_name(hass: HomeAssistant, test_entity_id: str) -> None:
"""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.attributes.get("friendly_name") == "Template Alarm Panel"
@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")])
@pytest.mark.parametrize(
"config",
[
{
"alarm_control_panel": {
"platform": "template",
"panels": {"test_template_panel": TEMPLATE_ALARM_CONFIG},
}
},
],
("count", "state_template"), [(1, "{{ states('alarm_control_panel.test') }}")]
)
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
)
@pytest.mark.parametrize(
"service",
@ -340,7 +566,7 @@ async def test_name(hass: HomeAssistant) -> None:
"alarm_trigger",
],
)
@pytest.mark.usefixtures("start_ha")
@pytest.mark.usefixtures("setup_state_panel")
async def test_actions(
hass: HomeAssistant, service, call_service_events: list[Event]
) -> None:
@ -348,128 +574,147 @@ async def test_actions(
await hass.services.async_call(
ALARM_DOMAIN,
service,
{"entity_id": TEMPLATE_NAME, "code": "1234"},
{"entity_id": TEST_ENTITY_ID, "code": "1234"},
blocking=True,
)
await hass.async_block_till_done()
assert len(call_service_events) == 1
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(
"config",
("panel_config", "style"),
[
{
"alarm_control_panel": {
"platform": "template",
"panels": {
"test_template_alarm_control_panel_01": {
"unique_id": "not-so-unique-anymore",
"value_template": "{{ true }}",
},
"test_template_alarm_control_panel_02": {
"unique_id": "not-so-unique-anymore",
"value_template": "{{ false }}",
},
(
{
"test_template_alarm_control_panel_01": {
"value_template": "{{ true }}",
**UNIQUE_ID_CONFIG,
},
"test_template_alarm_control_panel_02": {
"value_template": "{{ false }}",
**UNIQUE_ID_CONFIG,
},
},
},
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:
"""Test unique_id option only creates one alarm control panel per id."""
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(
("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",
True,
),
(
{
"alarm_control_panel": {
"platform": "template",
"panels": {
"test_template_panel": {
"value_template": "disarmed",
"code_format": "text",
}
},
}
},
{"code_format": "text"},
"text",
True,
),
(
{
"alarm_control_panel": {
"platform": "template",
"panels": {
"test_template_panel": {
"value_template": "disarmed",
"code_format": "no_code",
"code_arm_required": False,
}
},
}
"code_format": "no_code",
"code_arm_required": False,
},
None,
False,
),
(
{
"alarm_control_panel": {
"platform": "template",
"panels": {
"test_template_panel": {
"value_template": "disarmed",
"code_format": "text",
"code_arm_required": False,
}
},
}
"code_format": "text",
"code_arm_required": False,
},
"text",
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:
"""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_arm_required") == code_arm_required
@pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")])
@pytest.mark.parametrize(
"config",
[
{
"alarm_control_panel": {
"platform": "template",
"panels": {"test_template_panel": TEMPLATE_ALARM_CONFIG},
}
},
],
("count", "state_template"), [(1, "{{ states('alarm_control_panel.test') }}")]
)
@pytest.mark.parametrize(
"style", [ConfigurationStyle.LEGACY, ConfigurationStyle.MODERN]
)
@pytest.mark.parametrize(
("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(
hass: HomeAssistant,
count,
domain,
config,
restored_state,
initial_state,
count: int,
state_template: str,
style: ConfigurationStyle,
restored_state: str,
initial_state: str,
) -> None:
"""Test restoring template alarm control panel."""
@ -522,17 +767,7 @@ async def test_restore_state(
{},
)
mock_restore_cache(hass, (fake_state,))
with assert_setup_component(count, domain):
assert await async_setup_component(
hass,
domain,
config,
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
await async_setup_state_panel(hass, count, style, state_template)
state = hass.states.get("alarm_control_panel.test_template_panel")
assert state.state == initial_state