Add config flow to template fan platform (#149446)

Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
This commit is contained in:
Petro31 2025-07-30 10:17:49 -04:00 committed by GitHub
parent fc900a632a
commit 160b61e0b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 243 additions and 4 deletions

View File

@ -72,6 +72,14 @@ from .cover import (
STOP_ACTION, STOP_ACTION,
async_create_preview_cover, async_create_preview_cover,
) )
from .fan import (
CONF_OFF_ACTION,
CONF_ON_ACTION,
CONF_PERCENTAGE,
CONF_SET_PERCENTAGE_ACTION,
CONF_SPEED_COUNT,
async_create_preview_fan,
)
from .light import ( from .light import (
CONF_HS, CONF_HS,
CONF_HS_ACTION, CONF_HS_ACTION,
@ -182,6 +190,19 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
) )
} }
if domain == Platform.FAN:
schema |= _SCHEMA_STATE | {
vol.Required(CONF_ON_ACTION): selector.ActionSelector(),
vol.Required(CONF_OFF_ACTION): selector.ActionSelector(),
vol.Optional(CONF_PERCENTAGE): selector.TemplateSelector(),
vol.Optional(CONF_SET_PERCENTAGE_ACTION): selector.ActionSelector(),
vol.Optional(CONF_SPEED_COUNT): selector.NumberSelector(
selector.NumberSelectorConfig(
min=1, max=100, step=1, mode=selector.NumberSelectorMode.BOX
),
),
}
if domain == Platform.IMAGE: if domain == Platform.IMAGE:
schema |= { schema |= {
vol.Required(CONF_URL): selector.TemplateSelector(), vol.Required(CONF_URL): selector.TemplateSelector(),
@ -379,6 +400,7 @@ TEMPLATE_TYPES = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
Platform.BUTTON, Platform.BUTTON,
Platform.COVER, Platform.COVER,
Platform.FAN,
Platform.IMAGE, Platform.IMAGE,
Platform.LIGHT, Platform.LIGHT,
Platform.NUMBER, Platform.NUMBER,
@ -408,6 +430,11 @@ CONFIG_FLOW = {
preview="template", preview="template",
validate_user_input=validate_user_input(Platform.COVER), validate_user_input=validate_user_input(Platform.COVER),
), ),
Platform.FAN: SchemaFlowFormStep(
config_schema(Platform.FAN),
preview="template",
validate_user_input=validate_user_input(Platform.FAN),
),
Platform.IMAGE: SchemaFlowFormStep( Platform.IMAGE: SchemaFlowFormStep(
config_schema(Platform.IMAGE), config_schema(Platform.IMAGE),
preview="template", preview="template",
@ -462,6 +489,11 @@ OPTIONS_FLOW = {
preview="template", preview="template",
validate_user_input=validate_user_input(Platform.COVER), validate_user_input=validate_user_input(Platform.COVER),
), ),
Platform.FAN: SchemaFlowFormStep(
options_schema(Platform.FAN),
preview="template",
validate_user_input=validate_user_input(Platform.FAN),
),
Platform.IMAGE: SchemaFlowFormStep( Platform.IMAGE: SchemaFlowFormStep(
options_schema(Platform.IMAGE), options_schema(Platform.IMAGE),
preview="template", preview="template",
@ -501,6 +533,7 @@ CREATE_PREVIEW_ENTITY: dict[
Platform.ALARM_CONTROL_PANEL: async_create_preview_alarm_control_panel, Platform.ALARM_CONTROL_PANEL: async_create_preview_alarm_control_panel,
Platform.BINARY_SENSOR: async_create_preview_binary_sensor, Platform.BINARY_SENSOR: async_create_preview_binary_sensor,
Platform.COVER: async_create_preview_cover, Platform.COVER: async_create_preview_cover,
Platform.FAN: async_create_preview_fan,
Platform.LIGHT: async_create_preview_light, Platform.LIGHT: async_create_preview_light,
Platform.NUMBER: async_create_preview_number, Platform.NUMBER: async_create_preview_number,
Platform.SELECT: async_create_preview_select, Platform.SELECT: async_create_preview_select,

View File

@ -20,6 +20,7 @@ from homeassistant.components.fan import (
FanEntity, FanEntity,
FanEntityFeature, FanEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_ENTITY_ID, CONF_ENTITY_ID,
CONF_FRIENDLY_NAME, CONF_FRIENDLY_NAME,
@ -34,15 +35,23 @@ 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, template 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.helpers.typing import ConfigType, DiscoveryInfoType
from .const import DOMAIN from .const import DOMAIN
from .coordinator import TriggerUpdateCoordinator from .coordinator import TriggerUpdateCoordinator
from .entity import AbstractTemplateEntity 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 ( from .template_entity import (
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY, TEMPLATE_ENTITY_AVAILABILITY_SCHEMA_LEGACY,
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA,
TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA, TEMPLATE_ENTITY_OPTIMISTIC_SCHEMA,
TemplateEntity, TemplateEntity,
make_template_entity_common_modern_schema, make_template_entity_common_modern_schema,
@ -132,6 +141,10 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(
{vol.Required(CONF_FANS): cv.schema_with_slug_keys(FAN_LEGACY_YAML_SCHEMA)} {vol.Required(CONF_FANS): cv.schema_with_slug_keys(FAN_LEGACY_YAML_SCHEMA)}
) )
FAN_CONFIG_ENTRY_SCHEMA = FAN_COMMON_SCHEMA.extend(
TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema
)
async def async_setup_platform( async def async_setup_platform(
hass: HomeAssistant, hass: HomeAssistant,
@ -153,6 +166,35 @@ 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,
StateFanEntity,
FAN_CONFIG_ENTRY_SCHEMA,
)
@callback
def async_create_preview_fan(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> StateFanEntity:
"""Create a preview."""
return async_setup_template_preview(
hass,
name,
config,
StateFanEntity,
FAN_CONFIG_ENTRY_SCHEMA,
)
class AbstractTemplateFan(AbstractTemplateEntity, FanEntity): class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
"""Representation of a template fan features.""" """Representation of a template fan features."""

View File

@ -111,6 +111,36 @@
}, },
"title": "Template cover" "title": "Template cover"
}, },
"fan": {
"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%]",
"percentage": "Percentage",
"set_percentage": "Actions on set percentage",
"speed_count": "Speed count"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "Defines a template to get the state of the fan. Valid values: `on`, `off`.",
"turn_off": "Defines actions to run when the fan is turned off.",
"turn_on": "Defines actions to run when the fan is turned on.",
"percentage": "Defines a template to get the speed percentage of the fan.",
"set_percentage": "Defines actions to run when the fan is given a speed percentage command.",
"speed_count": "The number of speeds the fan supports. Used to calculate the percentage step for the `fan.increase_speed` and `fan.decrease_speed` actions."
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
}
}
},
"title": "Template fan"
},
"image": { "image": {
"data": { "data": {
"device_id": "[%key:common::config_flow::data::device%]", "device_id": "[%key:common::config_flow::data::device%]",
@ -232,6 +262,7 @@
"binary_sensor": "Template a binary sensor", "binary_sensor": "Template a binary sensor",
"button": "Template a button", "button": "Template a button",
"cover": "Template a cover", "cover": "Template a cover",
"fan": "Template a fan",
"image": "Template an image", "image": "Template an image",
"light": "Template a light", "light": "Template a light",
"number": "Template a number", "number": "Template a number",
@ -360,6 +391,35 @@
}, },
"title": "[%key:component::template::config::step::cover::title%]" "title": "[%key:component::template::config::step::cover::title%]"
}, },
"fan": {
"data": {
"device_id": "[%key:common::config_flow::data::device%]",
"state": "[%key:component::template::common::state%]",
"turn_off": "[%key:component::template::common::turn_off%]",
"turn_on": "[%key:component::template::common::turn_on%]",
"percentage": "[%key:component::template::config::step::fan::data::percentage%]",
"set_percentage": "[%key:component::template::config::step::fan::data::set_percentage%]",
"speed_count": "[%key:component::template::config::step::fan::data::speed_count%]"
},
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]",
"state": "[%key:component::template::config::step::fan::data_description::state%]",
"turn_off": "[%key:component::template::config::step::fan::data_description::turn_off%]",
"turn_on": "[%key:component::template::config::step::fan::data_description::turn_on%]",
"percentage": "[%key:component::template::config::step::fan::data_description::percentage%]",
"set_percentage": "[%key:component::template::config::step::fan::data_description::set_percentage%]",
"speed_count": "[%key:component::template::config::step::fan::data_description::speed_count%]"
},
"sections": {
"advanced_options": {
"name": "[%key:component::template::common::advanced_options%]",
"data": {
"availability": "[%key:component::template::common::availability%]"
}
}
},
"title": "[%key:component::template::config::step::fan::title%]"
},
"image": { "image": {
"data": { "data": {
"device_id": "[%key:common::config_flow::data::device%]", "device_id": "[%key:common::config_flow::data::device%]",

View File

@ -0,0 +1,15 @@
# serializer version: 1
# name: test_setup_config_entry
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'My template',
'supported_features': <FanEntityFeature: 48>,
}),
'context': <ANY>,
'entity_id': 'fan.my_template',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@ -149,6 +149,16 @@ BINARY_SENSOR_OPTIONS = {
}, },
{}, {},
), ),
(
"fan",
{"state": "{{ states('fan.one') }}"},
"on",
{"one": "on", "two": "off"},
{},
{"turn_on": [], "turn_off": []},
{"turn_on": [], "turn_off": []},
{},
),
( (
"image", "image",
{"url": "{{ states('sensor.one') }}"}, {"url": "{{ states('sensor.one') }}"},
@ -332,6 +342,12 @@ async def test_config_flow(
{"set_cover_position": []}, {"set_cover_position": []},
{"set_cover_position": []}, {"set_cover_position": []},
), ),
(
"fan",
{"state": "{{ states('fan.one') }}"},
{"turn_on": [], "turn_off": []},
{"turn_on": [], "turn_off": []},
),
( (
"image", "image",
{ {
@ -534,6 +550,16 @@ async def test_config_flow_device(
{"set_cover_position": []}, {"set_cover_position": []},
"state", "state",
), ),
(
"fan",
{"state": "{{ states('fan.one') }}"},
{"state": "{{ states('fan.two') }}"},
["on", "off"],
{"one": "on", "two": "off"},
{"turn_on": [], "turn_off": []},
{"turn_on": [], "turn_off": []},
"state",
),
( (
"image", "image",
{ {
@ -1391,6 +1417,12 @@ async def test_option_flow_sensor_preview_config_entry_removed(
{"set_cover_position": []}, {"set_cover_position": []},
{"set_cover_position": []}, {"set_cover_position": []},
), ),
(
"fan",
{"state": "{{ states('fan.one') }}"},
{"turn_on": [], "turn_off": []},
{"turn_on": [], "turn_off": []},
),
( (
"image", "image",
{ {

View File

@ -3,6 +3,7 @@
from typing import Any from typing import Any
import pytest import pytest
from syrupy.assertion import SnapshotAssertion
import voluptuous as vol import voluptuous as vol
from homeassistant.components import fan, template from homeassistant.components import fan, template
@ -21,10 +22,11 @@ from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component 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.components.fan import common from tests.components.fan import common
from tests.typing import WebSocketGenerator
TEST_OBJECT_ID = "test_fan" TEST_OBJECT_ID = "test_fan"
TEST_ENTITY_ID = f"fan.{TEST_OBJECT_ID}" TEST_ENTITY_ID = f"fan.{TEST_OBJECT_ID}"
@ -1881,3 +1883,58 @@ async def test_optimistic_option(hass: HomeAssistant) -> None:
state = hass.states.get(TEST_ENTITY_ID) state = hass.states.get(TEST_ENTITY_ID)
assert state.state == STATE_OFF assert state.state == STATE_OFF
async def test_setup_config_entry(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
) -> None:
"""Tests creating a fan 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": fan.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("fan.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,
fan.DOMAIN,
{
"name": "My template",
"state": "{{ 'on' }}",
"turn_on": [],
"turn_off": [],
},
)
assert state["state"] == STATE_ON