Add preview support to all groups (#98951)
This commit is contained in:
parent
a741298461
commit
3e02fb1f07
9 changed files with 194 additions and 26 deletions
|
@ -24,7 +24,14 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
|||
from . import DOMAIN, GroupEntity
|
||||
from .binary_sensor import CONF_ALL, async_create_preview_binary_sensor
|
||||
from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC
|
||||
from .cover import async_create_preview_cover
|
||||
from .event import async_create_preview_event
|
||||
from .fan import async_create_preview_fan
|
||||
from .light import async_create_preview_light
|
||||
from .lock import async_create_preview_lock
|
||||
from .media_player import MediaPlayerGroup, async_create_preview_media_player
|
||||
from .sensor import async_create_preview_sensor
|
||||
from .switch import async_create_preview_switch
|
||||
|
||||
_STATISTIC_MEASURES = [
|
||||
"min",
|
||||
|
@ -122,7 +129,7 @@ SENSOR_CONFIG_SCHEMA = basic_group_config_schema(
|
|||
|
||||
|
||||
async def light_switch_options_schema(
|
||||
domain: str, handler: SchemaCommonFlowHandler
|
||||
domain: str, handler: SchemaCommonFlowHandler | None
|
||||
) -> vol.Schema:
|
||||
"""Generate options schema."""
|
||||
return (await basic_group_options_schema(domain, handler)).extend(
|
||||
|
@ -177,26 +184,32 @@ CONFIG_FLOW = {
|
|||
),
|
||||
"cover": SchemaFlowFormStep(
|
||||
basic_group_config_schema("cover"),
|
||||
preview="group",
|
||||
validate_user_input=set_group_type("cover"),
|
||||
),
|
||||
"event": SchemaFlowFormStep(
|
||||
basic_group_config_schema("event"),
|
||||
preview="group",
|
||||
validate_user_input=set_group_type("event"),
|
||||
),
|
||||
"fan": SchemaFlowFormStep(
|
||||
basic_group_config_schema("fan"),
|
||||
preview="group",
|
||||
validate_user_input=set_group_type("fan"),
|
||||
),
|
||||
"light": SchemaFlowFormStep(
|
||||
basic_group_config_schema("light"),
|
||||
preview="group",
|
||||
validate_user_input=set_group_type("light"),
|
||||
),
|
||||
"lock": SchemaFlowFormStep(
|
||||
basic_group_config_schema("lock"),
|
||||
preview="group",
|
||||
validate_user_input=set_group_type("lock"),
|
||||
),
|
||||
"media_player": SchemaFlowFormStep(
|
||||
basic_group_config_schema("media_player"),
|
||||
preview="group",
|
||||
validate_user_input=set_group_type("media_player"),
|
||||
),
|
||||
"sensor": SchemaFlowFormStep(
|
||||
|
@ -206,6 +219,7 @@ CONFIG_FLOW = {
|
|||
),
|
||||
"switch": SchemaFlowFormStep(
|
||||
basic_group_config_schema("switch"),
|
||||
preview="group",
|
||||
validate_user_input=set_group_type("switch"),
|
||||
),
|
||||
}
|
||||
|
@ -217,11 +231,26 @@ OPTIONS_FLOW = {
|
|||
binary_sensor_options_schema,
|
||||
preview="group",
|
||||
),
|
||||
"cover": SchemaFlowFormStep(partial(basic_group_options_schema, "cover")),
|
||||
"event": SchemaFlowFormStep(partial(basic_group_options_schema, "event")),
|
||||
"fan": SchemaFlowFormStep(partial(basic_group_options_schema, "fan")),
|
||||
"light": SchemaFlowFormStep(partial(light_switch_options_schema, "light")),
|
||||
"lock": SchemaFlowFormStep(partial(basic_group_options_schema, "lock")),
|
||||
"cover": SchemaFlowFormStep(
|
||||
partial(basic_group_options_schema, "cover"),
|
||||
preview="group",
|
||||
),
|
||||
"event": SchemaFlowFormStep(
|
||||
partial(basic_group_options_schema, "event"),
|
||||
preview="group",
|
||||
),
|
||||
"fan": SchemaFlowFormStep(
|
||||
partial(basic_group_options_schema, "fan"),
|
||||
preview="group",
|
||||
),
|
||||
"light": SchemaFlowFormStep(
|
||||
partial(light_switch_options_schema, "light"),
|
||||
preview="group",
|
||||
),
|
||||
"lock": SchemaFlowFormStep(
|
||||
partial(basic_group_options_schema, "lock"),
|
||||
preview="group",
|
||||
),
|
||||
"media_player": SchemaFlowFormStep(
|
||||
partial(basic_group_options_schema, "media_player"),
|
||||
preview="group",
|
||||
|
@ -230,17 +259,27 @@ OPTIONS_FLOW = {
|
|||
partial(sensor_options_schema, "sensor"),
|
||||
preview="group",
|
||||
),
|
||||
"switch": SchemaFlowFormStep(partial(light_switch_options_schema, "switch")),
|
||||
"switch": SchemaFlowFormStep(
|
||||
partial(light_switch_options_schema, "switch"),
|
||||
preview="group",
|
||||
),
|
||||
}
|
||||
|
||||
PREVIEW_OPTIONS_SCHEMA: dict[str, vol.Schema] = {}
|
||||
|
||||
CREATE_PREVIEW_ENTITY: dict[
|
||||
str,
|
||||
Callable[[str, dict[str, Any]], GroupEntity],
|
||||
Callable[[str, dict[str, Any]], GroupEntity | MediaPlayerGroup],
|
||||
] = {
|
||||
"binary_sensor": async_create_preview_binary_sensor,
|
||||
"cover": async_create_preview_cover,
|
||||
"event": async_create_preview_event,
|
||||
"fan": async_create_preview_fan,
|
||||
"light": async_create_preview_light,
|
||||
"lock": async_create_preview_lock,
|
||||
"media_player": async_create_preview_media_player,
|
||||
"sensor": async_create_preview_sensor,
|
||||
"switch": async_create_preview_switch,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -96,6 +96,18 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_preview_cover(
|
||||
name: str, validated_config: dict[str, Any]
|
||||
) -> CoverGroup:
|
||||
"""Create a preview sensor."""
|
||||
return CoverGroup(
|
||||
None,
|
||||
name,
|
||||
validated_config[CONF_ENTITIES],
|
||||
)
|
||||
|
||||
|
||||
class CoverGroup(GroupEntity, CoverEntity):
|
||||
"""Representation of a CoverGroup."""
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -87,6 +88,18 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_preview_event(
|
||||
name: str, validated_config: dict[str, Any]
|
||||
) -> EventGroup:
|
||||
"""Create a preview sensor."""
|
||||
return EventGroup(
|
||||
None,
|
||||
name,
|
||||
validated_config[CONF_ENTITIES],
|
||||
)
|
||||
|
||||
|
||||
class EventGroup(GroupEntity, EventEntity):
|
||||
"""Representation of an event group."""
|
||||
|
||||
|
|
|
@ -96,6 +96,16 @@ async def async_setup_entry(
|
|||
async_add_entities([FanGroup(config_entry.entry_id, config_entry.title, entities)])
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_preview_fan(name: str, validated_config: dict[str, Any]) -> FanGroup:
|
||||
"""Create a preview sensor."""
|
||||
return FanGroup(
|
||||
None,
|
||||
name,
|
||||
validated_config[CONF_ENTITIES],
|
||||
)
|
||||
|
||||
|
||||
class FanGroup(GroupEntity, FanEntity):
|
||||
"""Representation of a FanGroup."""
|
||||
|
||||
|
|
|
@ -110,6 +110,19 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_preview_light(
|
||||
name: str, validated_config: dict[str, Any]
|
||||
) -> LightGroup:
|
||||
"""Create a preview sensor."""
|
||||
return LightGroup(
|
||||
None,
|
||||
name,
|
||||
validated_config[CONF_ENTITIES],
|
||||
validated_config.get(CONF_ALL, False),
|
||||
)
|
||||
|
||||
|
||||
FORWARDED_ATTRIBUTES = frozenset(
|
||||
{
|
||||
ATTR_BRIGHTNESS,
|
||||
|
|
|
@ -90,6 +90,16 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_preview_lock(name: str, validated_config: dict[str, Any]) -> LockGroup:
|
||||
"""Create a preview sensor."""
|
||||
return LockGroup(
|
||||
None,
|
||||
name,
|
||||
validated_config[CONF_ENTITIES],
|
||||
)
|
||||
|
||||
|
||||
class LockGroup(GroupEntity, LockEntity):
|
||||
"""Representation of a lock group."""
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Platform allowing several media players to be grouped into one media player."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Callable, Mapping
|
||||
from contextlib import suppress
|
||||
from typing import Any
|
||||
|
||||
|
@ -44,7 +44,7 @@ from homeassistant.const import (
|
|||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import (
|
||||
|
@ -107,6 +107,18 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_preview_media_player(
|
||||
name: str, validated_config: dict[str, Any]
|
||||
) -> MediaPlayerGroup:
|
||||
"""Create a preview sensor."""
|
||||
return MediaPlayerGroup(
|
||||
None,
|
||||
name,
|
||||
validated_config[CONF_ENTITIES],
|
||||
)
|
||||
|
||||
|
||||
class MediaPlayerGroup(MediaPlayerEntity):
|
||||
"""Representation of a Media Group."""
|
||||
|
||||
|
@ -139,7 +151,8 @@ class MediaPlayerGroup(MediaPlayerEntity):
|
|||
self.async_update_supported_features(
|
||||
event.data["entity_id"], event.data["new_state"]
|
||||
)
|
||||
self.async_update_state()
|
||||
self.async_update_group_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def async_update_supported_features(
|
||||
|
@ -208,6 +221,26 @@ class MediaPlayerGroup(MediaPlayerEntity):
|
|||
else:
|
||||
self._features[KEY_ENQUEUE].discard(entity_id)
|
||||
|
||||
@callback
|
||||
def async_start_preview(
|
||||
self,
|
||||
preview_callback: Callable[[str, Mapping[str, Any]], None],
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Render a preview."""
|
||||
|
||||
@callback
|
||||
def async_state_changed_listener(
|
||||
event: EventType[EventStateChangedData] | None,
|
||||
) -> None:
|
||||
"""Handle child updates."""
|
||||
self.async_update_group_state()
|
||||
preview_callback(*self._async_generate_attributes())
|
||||
|
||||
async_state_changed_listener(None)
|
||||
return async_track_state_change_event(
|
||||
self.hass, self._entities, async_state_changed_listener
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register listeners."""
|
||||
for entity_id in self._entities:
|
||||
|
@ -216,7 +249,8 @@ class MediaPlayerGroup(MediaPlayerEntity):
|
|||
async_track_state_change_event(
|
||||
self.hass, self._entities, self.async_on_state_change
|
||||
)
|
||||
self.async_update_state()
|
||||
self.async_update_group_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
@ -391,7 +425,7 @@ class MediaPlayerGroup(MediaPlayerEntity):
|
|||
await self.async_set_volume_level(max(0, volume_level - 0.1))
|
||||
|
||||
@callback
|
||||
def async_update_state(self) -> None:
|
||||
def async_update_group_state(self) -> None:
|
||||
"""Query all members and determine the media group state."""
|
||||
states = [
|
||||
state.state
|
||||
|
@ -455,4 +489,3 @@ class MediaPlayerGroup(MediaPlayerEntity):
|
|||
supported_features |= MediaPlayerEntityFeature.MEDIA_ENQUEUE
|
||||
|
||||
self._attr_supported_features = supported_features
|
||||
self.async_write_ha_state()
|
||||
|
|
|
@ -85,6 +85,19 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_preview_switch(
|
||||
name: str, validated_config: dict[str, Any]
|
||||
) -> SwitchGroup:
|
||||
"""Create a preview sensor."""
|
||||
return SwitchGroup(
|
||||
None,
|
||||
name,
|
||||
validated_config[CONF_ENTITIES],
|
||||
validated_config.get(CONF_ALL, False),
|
||||
)
|
||||
|
||||
|
||||
class SwitchGroup(GroupEntity, SwitchEntity):
|
||||
"""Representation of a switch group."""
|
||||
|
||||
|
|
|
@ -466,17 +466,34 @@ async def test_options_flow_hides_members(
|
|||
assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by
|
||||
|
||||
|
||||
COVER_ATTRS = [{"supported_features": 0}, {}]
|
||||
EVENT_ATTRS = [{"event_types": []}, {"event_type": None}]
|
||||
FAN_ATTRS = [{"supported_features": 0}, {"assumed_state": True}]
|
||||
LIGHT_ATTRS = [
|
||||
{
|
||||
"icon": "mdi:lightbulb-group",
|
||||
"supported_color_modes": ["onoff"],
|
||||
"supported_features": 0,
|
||||
},
|
||||
{"color_mode": "onoff"},
|
||||
]
|
||||
LOCK_ATTRS = [{"supported_features": 1}, {}]
|
||||
MEDIA_PLAYER_ATTRS = [{"supported_features": 0}, {}]
|
||||
SENSOR_ATTRS = [{"icon": "mdi:calculator"}, {"max_entity_id": "sensor.input_two"}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("domain", "extra_user_input", "input_states", "group_state", "extra_attributes"),
|
||||
[
|
||||
("binary_sensor", {"all": True}, ["on", "off"], "off", [{}, {}]),
|
||||
(
|
||||
"sensor",
|
||||
{"type": "max"},
|
||||
["10", "20"],
|
||||
"20.0",
|
||||
[{"icon": "mdi:calculator"}, {"max_entity_id": "sensor.input_two"}],
|
||||
),
|
||||
("cover", {}, ["open", "closed"], "open", COVER_ATTRS),
|
||||
("event", {}, ["", ""], "unknown", EVENT_ATTRS),
|
||||
("fan", {}, ["on", "off"], "on", FAN_ATTRS),
|
||||
("light", {}, ["on", "off"], "on", LIGHT_ATTRS),
|
||||
("lock", {}, ["unlocked", "locked"], "unlocked", LOCK_ATTRS),
|
||||
("media_player", {}, ["on", "off"], "on", MEDIA_PLAYER_ATTRS),
|
||||
("sensor", {"type": "max"}, ["10", "20"], "20.0", SENSOR_ATTRS),
|
||||
("switch", {}, ["on", "off"], "on", [{}, {}]),
|
||||
],
|
||||
)
|
||||
async def test_config_flow_preview(
|
||||
|
@ -553,15 +570,22 @@ async def test_config_flow_preview(
|
|||
"extra_attributes",
|
||||
),
|
||||
[
|
||||
("binary_sensor", {"all": True}, {"all": False}, ["on", "off"], "on", {}),
|
||||
("binary_sensor", {"all": True}, {"all": False}, ["on", "off"], "on", [{}, {}]),
|
||||
("cover", {}, {}, ["open", "closed"], "open", COVER_ATTRS),
|
||||
("event", {}, {}, ["", ""], "unknown", EVENT_ATTRS),
|
||||
("fan", {}, {}, ["on", "off"], "on", FAN_ATTRS),
|
||||
("light", {}, {}, ["on", "off"], "on", LIGHT_ATTRS),
|
||||
("lock", {}, {}, ["unlocked", "locked"], "unlocked", LOCK_ATTRS),
|
||||
("media_player", {}, {}, ["on", "off"], "on", MEDIA_PLAYER_ATTRS),
|
||||
(
|
||||
"sensor",
|
||||
{"type": "min"},
|
||||
{"type": "max"},
|
||||
["10", "20"],
|
||||
"20.0",
|
||||
{"icon": "mdi:calculator", "max_entity_id": "sensor.input_two"},
|
||||
SENSOR_ATTRS,
|
||||
),
|
||||
("switch", {}, {}, ["on", "off"], "on", [{}, {}]),
|
||||
],
|
||||
)
|
||||
async def test_option_flow_preview(
|
||||
|
@ -575,8 +599,6 @@ async def test_option_flow_preview(
|
|||
extra_attributes: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test the option flow preview."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
input_entities = [f"{domain}.input_one", f"{domain}.input_two"]
|
||||
|
||||
# Setup the config entry
|
||||
|
@ -596,6 +618,8 @@ async def test_option_flow_preview(
|
|||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
@ -619,7 +643,8 @@ async def test_option_flow_preview(
|
|||
msg = await client.receive_json()
|
||||
assert msg["event"] == {
|
||||
"attributes": {"entity_id": input_entities, "friendly_name": "My group"}
|
||||
| extra_attributes,
|
||||
| extra_attributes[0]
|
||||
| extra_attributes[1],
|
||||
"state": group_state,
|
||||
}
|
||||
assert len(hass.states.async_all()) == 3
|
||||
|
|
Loading…
Add table
Reference in a new issue