diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index e7fe3887ce9..0d9e5ebc8ce 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -15,8 +15,10 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, CodeFormat, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CODE, + CONF_DEVICE_ID, CONF_NAME, CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, @@ -34,12 +36,14 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError +from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.script import Script 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 @@ -105,6 +109,25 @@ PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend( } ) +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(), + } +) + async def _async_create_entities( hass: HomeAssistant, config: dict[str, Any] @@ -128,6 +151,27 @@ async def _async_create_entities( return alarm_control_panels +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize config entry.""" + _options = dict(config_entry.options) + _options.pop("template_type") + validated_config = ALARM_CONTROL_PANEL_CONFIG_SCHEMA(_options) + async_add_entities( + [ + AlarmControlPanelTemplate( + hass, + slugify(_options[CONF_NAME]), + validated_config, + config_entry.entry_id, + ) + ] + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index a8a7c1b9971..c1c023c0ea4 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -39,6 +39,18 @@ from homeassistant.helpers.schema_config_entry_flow import ( SchemaFlowMenuStep, ) +from .alarm_control_panel import ( + CONF_ARM_AWAY_ACTION, + CONF_ARM_CUSTOM_BYPASS_ACTION, + CONF_ARM_HOME_ACTION, + CONF_ARM_NIGHT_ACTION, + CONF_ARM_VACATION_ACTION, + CONF_CODE_ARM_REQUIRED, + CONF_CODE_FORMAT, + CONF_DISARM_ACTION, + CONF_TRIGGER_ACTION, + TemplateCodeFormat, +) from .binary_sensor import async_create_preview_binary_sensor from .const import CONF_PRESS, CONF_TURN_OFF, CONF_TURN_ON, DOMAIN from .number import ( @@ -68,6 +80,30 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: if flow_type == "config": schema = {vol.Required(CONF_NAME): selector.TextSelector()} + if domain == Platform.ALARM_CONTROL_PANEL: + schema |= { + vol.Optional(CONF_VALUE_TEMPLATE): selector.TemplateSelector(), + vol.Optional(CONF_DISARM_ACTION): selector.ActionSelector(), + vol.Optional(CONF_ARM_AWAY_ACTION): selector.ActionSelector(), + vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): selector.ActionSelector(), + vol.Optional(CONF_ARM_HOME_ACTION): selector.ActionSelector(), + vol.Optional(CONF_ARM_NIGHT_ACTION): selector.ActionSelector(), + vol.Optional(CONF_ARM_VACATION_ACTION): selector.ActionSelector(), + vol.Optional(CONF_TRIGGER_ACTION): selector.ActionSelector(), + vol.Optional( + CONF_CODE_ARM_REQUIRED, default=True + ): selector.BooleanSelector(), + vol.Optional( + CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name + ): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[e.name for e in TemplateCodeFormat], + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key="alarm_control_panel_code_format", + ) + ), + } + if domain == Platform.BINARY_SENSOR: schema |= _SCHEMA_STATE if flow_type == "config": @@ -265,6 +301,7 @@ def validate_user_input( TEMPLATE_TYPES = [ + "alarm_control_panel", "binary_sensor", "button", "image", @@ -276,6 +313,10 @@ TEMPLATE_TYPES = [ CONFIG_FLOW = { "user": SchemaFlowMenuStep(TEMPLATE_TYPES), + Platform.ALARM_CONTROL_PANEL: SchemaFlowFormStep( + config_schema(Platform.ALARM_CONTROL_PANEL), + validate_user_input=validate_user_input(Platform.ALARM_CONTROL_PANEL), + ), Platform.BINARY_SENSOR: SchemaFlowFormStep( config_schema(Platform.BINARY_SENSOR), preview="template", @@ -313,6 +354,10 @@ CONFIG_FLOW = { OPTIONS_FLOW = { "init": SchemaFlowFormStep(next_step=choose_options_step), + Platform.ALARM_CONTROL_PANEL: SchemaFlowFormStep( + options_schema(Platform.ALARM_CONTROL_PANEL), + validate_user_input=validate_user_input(Platform.ALARM_CONTROL_PANEL), + ), Platform.BINARY_SENSOR: SchemaFlowFormStep( options_schema(Platform.BINARY_SENSOR), preview="template", diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 4a79ee62d30..26a6ba61704 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -1,6 +1,26 @@ { "config": { "step": { + "alarm_control_panel": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "value_template": "[%key:component::template::config::step::switch::data::value_template%]", + "name": "[%key:common::config_flow::data::name%]", + "disarm": "Disarm action", + "arm_away": "Arm away action", + "arm_custom_bypass": "Arm custom bypass action", + "arm_home": "Arm home action", + "arm_night": "Arm night action", + "arm_vacation": "Arm vacation action", + "trigger": "Trigger action", + "code_arm_required": "Code arm required", + "code_format": "Code format" + }, + "data_description": { + "device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" + }, + "title": "Template alarm control panel" + }, "binary_sensor": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -111,6 +131,25 @@ }, "options": { "step": { + "alarm_control_panel": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "value_template": "[%key:component::template::config::step::switch::data::value_template%]", + "disarm": "[%key:component::template::config::step::alarm_control_panel::data::disarm%]", + "arm_away": "[%key:component::template::config::step::alarm_control_panel::data::arm_away%]", + "arm_custom_bypass": "[%key:component::template::config::step::alarm_control_panel::data::arm_custom_bypass%]", + "arm_home": "[%key:component::template::config::step::alarm_control_panel::data::arm_home%]", + "arm_night": "[%key:component::template::config::step::alarm_control_panel::data::arm_night%]", + "arm_vacation": "[%key:component::template::config::step::alarm_control_panel::data::arm_vacation%]", + "trigger": "[%key:component::template::config::step::alarm_control_panel::data::trigger%]", + "code_arm_required": "[%key:component::template::config::step::alarm_control_panel::data::code_arm_required%]", + "code_format": "[%key:component::template::config::step::alarm_control_panel::data::code_format%]" + }, + "data_description": { + "device_id": "[%key:component::template::config::step::sensor::data_description::device_id%]" + }, + "title": "[%key:component::template::config::step::alarm_control_panel::title%]" + }, "binary_sensor": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -200,6 +239,13 @@ } }, "selector": { + "alarm_control_panel_code_format": { + "options": { + "no_code": "No code format", + "number": "Number", + "text": "Text" + } + }, "binary_sensor_device_class": { "options": { "battery": "[%key:component::binary_sensor::entity_component::battery::name%]", diff --git a/tests/components/template/snapshots/test_alarm_control_panel.ambr b/tests/components/template/snapshots/test_alarm_control_panel.ambr new file mode 100644 index 00000000000..9772c31220e --- /dev/null +++ b/tests/components/template/snapshots/test_alarm_control_panel.ambr @@ -0,0 +1,18 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'changed_by': None, + 'code_arm_required': True, + 'code_format': , + 'friendly_name': 'My template', + 'supported_features': , + }), + 'context': , + 'entity_id': 'alarm_control_panel.my_template', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'armed_away', + }) +# --- diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index ac9bb2dcb36..1532197d738 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -1,7 +1,9 @@ """The tests for the Template alarm control panel platform.""" import pytest +from syrupy.assertion import SnapshotAssertion +from homeassistant.components import template from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN from homeassistant.const import ( ATTR_DOMAIN, @@ -23,7 +25,7 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.setup import async_setup_component -from tests.common import 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" PANEL_NAME = "alarm_control_panel.test" @@ -130,6 +132,41 @@ async def test_template_state_text(hass: HomeAssistant, start_ha) -> None: assert state.state == "unknown" +async def test_setup_config_entry( + hass: HomeAssistant, snapshot: SnapshotAssertion +) -> None: + """Test the config flow.""" + value_template = "{{ states('alarm_control_panel.one') }}" + + hass.states.async_set("alarm_control_panel.one", "armed_away", {}) + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "value_template": value_template, + "template_type": "alarm_control_panel", + "code_arm_required": True, + "code_format": "number", + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.my_template") + assert state is not None + assert state == snapshot + + hass.states.async_set("alarm_control_panel.one", "disarmed", {}) + await hass.async_block_till_done() + state = hass.states.get("alarm_control_panel.my_template") + assert state.state == STATE_ALARM_DISARMED + + @pytest.mark.parametrize(("count", "domain"), [(1, "alarm_control_panel")]) @pytest.mark.parametrize( "config", diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 380a0a8f53e..713e27e653f 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -29,6 +29,16 @@ from tests.typing import WebSocketGenerator "extra_attrs", ), [ + ( + "alarm_control_panel", + {"value_template": "{{ states('alarm_control_panel.one') }}"}, + "armed_away", + {"one": "armed_away", "two": "disarmed"}, + {}, + {}, + {"code_arm_required": True, "code_format": "number"}, + {}, + ), ( "binary_sensor", { @@ -270,6 +280,12 @@ async def test_config_flow( "step": 0.1, }, ), + ( + "alarm_control_panel", + {"value_template": "{{ states('alarm_control_panel.one') }}"}, + {"code_arm_required": True, "code_format": "number"}, + {"code_arm_required": True, "code_format": "number"}, + ), ( "select", {"state": "{{ states('select.one') }}"}, @@ -476,6 +492,16 @@ def get_suggested(schema, key): }, "state", ), + ( + "alarm_control_panel", + {"value_template": "{{ states('alarm_control_panel.one') }}"}, + {"value_template": "{{ states('alarm_control_panel.two') }}"}, + ["armed_away", "disarmed"], + {"one": "armed_away", "two": "disarmed"}, + {"code_arm_required": True, "code_format": "number"}, + {"code_arm_required": True, "code_format": "number"}, + "value_template", + ), ( "select", {"state": "{{ states('select.one') }}"}, @@ -1244,6 +1270,12 @@ async def test_option_flow_sensor_preview_config_entry_removed( "step": 0.1, }, ), + ( + "alarm_control_panel", + {"value_template": "{{ states('alarm_control_panel.one') }}"}, + {"code_arm_required": True, "code_format": "number"}, + {"code_arm_required": True, "code_format": "number"}, + ), ( "select", {"state": "{{ states('select.one') }}"},