From a21af78aa1391e815dd360dbfd46acfca84a9b01 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Wed, 30 Jul 2025 09:27:43 -0400 Subject: [PATCH] Add config flow to template light platform (#149448) --- .../components/template/config_flow.py | 33 ++++++++++ homeassistant/components/template/light.py | 50 ++++++++++++++- .../components/template/strings.json | 55 +++++++++++++++++ .../template/snapshots/test_light.ambr | 19 ++++++ tests/components/template/test_config_flow.py | 32 ++++++++++ tests/components/template/test_light.py | 61 ++++++++++++++++++- 6 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 tests/components/template/snapshots/test_light.ambr diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 7963f525b7a..c9028d058bf 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -72,6 +72,15 @@ from .cover import ( STOP_ACTION, async_create_preview_cover, ) +from .light import ( + CONF_HS, + CONF_HS_ACTION, + CONF_LEVEL, + CONF_LEVEL_ACTION, + CONF_TEMPERATURE, + CONF_TEMPERATURE_ACTION, + async_create_preview_light, +) from .number import ( CONF_MAX, CONF_MIN, @@ -179,6 +188,18 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: vol.Optional(CONF_VERIFY_SSL, default=True): selector.BooleanSelector(), } + if domain == Platform.LIGHT: + schema |= _SCHEMA_STATE | { + vol.Required(CONF_TURN_ON): selector.ActionSelector(), + vol.Required(CONF_TURN_OFF): selector.ActionSelector(), + vol.Optional(CONF_LEVEL): selector.TemplateSelector(), + vol.Optional(CONF_LEVEL_ACTION): selector.ActionSelector(), + vol.Optional(CONF_HS): selector.TemplateSelector(), + vol.Optional(CONF_HS_ACTION): selector.ActionSelector(), + vol.Optional(CONF_TEMPERATURE): selector.TemplateSelector(), + vol.Optional(CONF_TEMPERATURE_ACTION): selector.ActionSelector(), + } + if domain == Platform.NUMBER: schema |= { vol.Required(CONF_STATE): selector.TemplateSelector(), @@ -359,6 +380,7 @@ TEMPLATE_TYPES = [ Platform.BUTTON, Platform.COVER, Platform.IMAGE, + Platform.LIGHT, Platform.NUMBER, Platform.SELECT, Platform.SENSOR, @@ -391,6 +413,11 @@ CONFIG_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.IMAGE), ), + Platform.LIGHT: SchemaFlowFormStep( + config_schema(Platform.LIGHT), + preview="template", + validate_user_input=validate_user_input(Platform.LIGHT), + ), Platform.NUMBER: SchemaFlowFormStep( config_schema(Platform.NUMBER), preview="template", @@ -440,6 +467,11 @@ OPTIONS_FLOW = { preview="template", validate_user_input=validate_user_input(Platform.IMAGE), ), + Platform.LIGHT: SchemaFlowFormStep( + options_schema(Platform.LIGHT), + preview="template", + validate_user_input=validate_user_input(Platform.LIGHT), + ), Platform.NUMBER: SchemaFlowFormStep( options_schema(Platform.NUMBER), preview="template", @@ -469,6 +501,7 @@ CREATE_PREVIEW_ENTITY: dict[ Platform.ALARM_CONTROL_PANEL: async_create_preview_alarm_control_panel, Platform.BINARY_SENSOR: async_create_preview_binary_sensor, Platform.COVER: async_create_preview_cover, + Platform.LIGHT: async_create_preview_light, Platform.NUMBER: async_create_preview_number, Platform.SELECT: async_create_preview_select, Platform.SENSOR: async_create_preview_sensor, diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 19eecaa7006..538d3f3aaaf 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -27,6 +27,7 @@ from homeassistant.components.light import ( LightEntityFeature, filter_supported_color_modes, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_EFFECT, CONF_ENTITY_ID, @@ -43,15 +44,23 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import ( + AddConfigEntryEntitiesCallback, + AddEntitiesCallback, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import color as color_util from . import TriggerUpdateCoordinator from .const import DOMAIN from .entity import AbstractTemplateEntity -from .helpers import async_setup_template_platform +from .helpers import ( + async_setup_template_entry, + async_setup_template_platform, + async_setup_template_preview, +) from .template_entity import ( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TemplateEntity, @@ -135,6 +144,8 @@ LIGHT_COMMON_SCHEMA = vol.Schema( vol.Optional(CONF_MIN_MIREDS): cv.template, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_RGB): cv.template, vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA, @@ -195,6 +206,10 @@ PLATFORM_SCHEMA = vol.All( ), ) +LIGHT_CONFIG_ENTRY_SCHEMA = LIGHT_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema +) + async def async_setup_platform( hass: HomeAssistant, @@ -216,6 +231,37 @@ async def async_setup_platform( ) +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Initialize config entry.""" + await async_setup_template_entry( + hass, + config_entry, + async_add_entities, + StateLightEntity, + LIGHT_CONFIG_ENTRY_SCHEMA, + True, + ) + + +@callback +def async_create_preview_light( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> StateLightEntity: + """Create a preview.""" + return async_setup_template_preview( + hass, + name, + config, + StateLightEntity, + LIGHT_CONFIG_ENTRY_SCHEMA, + True, + ) + + class AbstractTemplateLight(AbstractTemplateEntity, LightEntity): """Representation of a template lights features.""" diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 36bca174ef6..070dd75865f 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -131,6 +131,33 @@ }, "title": "Template image" }, + "light": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::common::state%]", + "turn_off": "[%key:component::template::common::turn_off%]", + "turn_on": "[%key:component::template::common::turn_on%]", + "level": "Brightness level", + "set_level": "Actions on set level", + "hs": "HS color", + "set_hs": "Actions on set HS color", + "temperature": "Color temperature", + "set_temperature": "Actions on set color temperature" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + } + } + }, + "title": "Template light" + }, "number": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -206,6 +233,7 @@ "button": "Template a button", "cover": "Template a cover", "image": "Template an image", + "light": "Template a light", "number": "Template a number", "select": "Template a select", "sensor": "Template a sensor", @@ -351,6 +379,33 @@ }, "title": "[%key:component::template::config::step::image::title%]" }, + "light": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "name": "[%key:common::config_flow::data::name%]", + "state": "[%key:component::template::common::state%]", + "turn_off": "[%key:component::template::common::turn_off%]", + "turn_on": "[%key:component::template::common::turn_on%]", + "level": "[%key:component::template::config::step::light::data::level%]", + "set_level": "[%key:component::template::config::step::light::data::set_level%]", + "hs": "[%key:component::template::config::step::light::data::hs%]", + "set_hs": "[%key:component::template::config::step::light::data::set_hs%]", + "temperature": "[%key:component::template::config::step::light::data::temperature%]", + "set_temperature": "[%key:component::template::config::step::light::data::set_temperature%]" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + } + } + }, + "title": "[%key:component::template::config::step::light::title%]" + }, "number": { "data": { "device_id": "[%key:common::config_flow::data::device%]", diff --git a/tests/components/template/snapshots/test_light.ambr b/tests/components/template/snapshots/test_light.ambr new file mode 100644 index 00000000000..0740d56a72e --- /dev/null +++ b/tests/components/template/snapshots/test_light.ambr @@ -0,0 +1,19 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'color_mode': , + 'friendly_name': 'My template', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.my_template', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 8d7f2e6d89c..ad992eec79d 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -159,6 +159,16 @@ BINARY_SENSOR_OPTIONS = { {"verify_ssl": True}, {}, ), + ( + "light", + {"state": "{{ states('light.one') }}"}, + "on", + {"one": "on", "two": "off"}, + {}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + {}, + ), ( "number", {"state": "{{ states('number.one') }}"}, @@ -330,6 +340,12 @@ async def test_config_flow( {"verify_ssl": True}, {"verify_ssl": True}, ), + ( + "light", + {"state": "{{ states('light.one') }}"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + ), ( "number", {"state": "{{ states('number.one') }}"}, @@ -535,6 +551,16 @@ async def test_config_flow_device( }, "url", ), + ( + "light", + {"state": "{{ states('light.one') }}"}, + {"state": "{{ states('light.two') }}"}, + ["on", "off"], + {"one": "on", "two": "off"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + "state", + ), ( "number", {"state": "{{ states('number.one') }}"}, @@ -1374,6 +1400,12 @@ async def test_option_flow_sensor_preview_config_entry_removed( {}, {}, ), + ( + "light", + {"state": "{{ states('light.one') }}"}, + {"turn_on": [], "turn_off": []}, + {"turn_on": [], "turn_off": []}, + ), ( "number", {"state": "{{ states('number.one') }}"}, diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index b42eba0665d..0549f9981e7 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -3,6 +3,7 @@ from typing import Any import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components import light, template from homeassistant.components.light import ( @@ -30,9 +31,10 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from .conftest import ConfigurationStyle +from .conftest import ConfigurationStyle, async_get_flow_preview_state -from tests.common import assert_setup_component +from tests.common import MockConfigEntry, assert_setup_component +from tests.typing import WebSocketGenerator # Represent for light's availability _STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" @@ -2791,3 +2793,58 @@ async def test_optimistic_option(hass: HomeAssistant) -> None: state = hass.states.get(TEST_ENTITY_ID) assert state.state == STATE_OFF + + +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Tests creating a light from a config entry.""" + + hass.states.async_set( + "sensor.test_sensor", + "on", + {}, + ) + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": "My template", + "state": "{{ states('sensor.test_sensor') }}", + "turn_on": [], + "turn_off": [], + "template_type": light.DOMAIN, + }, + 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("light.my_template") + assert state is not None + assert state == snapshot + + +async def test_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the config flow preview.""" + + state = await async_get_flow_preview_state( + hass, + hass_ws_client, + light.DOMAIN, + { + "name": "My template", + "state": "{{ 'on' }}", + "turn_on": [], + "turn_off": [], + }, + ) + + assert state["state"] == STATE_ON