Add preview support to all groups (#98951)

This commit is contained in:
Erik Montnemery 2023-08-25 08:59:33 +02:00 committed by GitHub
parent a741298461
commit 3e02fb1f07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 194 additions and 26 deletions

View file

@ -24,7 +24,14 @@ from homeassistant.helpers.schema_config_entry_flow import (
from . import DOMAIN, GroupEntity from . import DOMAIN, GroupEntity
from .binary_sensor import CONF_ALL, async_create_preview_binary_sensor from .binary_sensor import CONF_ALL, async_create_preview_binary_sensor
from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC 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 .sensor import async_create_preview_sensor
from .switch import async_create_preview_switch
_STATISTIC_MEASURES = [ _STATISTIC_MEASURES = [
"min", "min",
@ -122,7 +129,7 @@ SENSOR_CONFIG_SCHEMA = basic_group_config_schema(
async def light_switch_options_schema( async def light_switch_options_schema(
domain: str, handler: SchemaCommonFlowHandler domain: str, handler: SchemaCommonFlowHandler | None
) -> vol.Schema: ) -> vol.Schema:
"""Generate options schema.""" """Generate options schema."""
return (await basic_group_options_schema(domain, handler)).extend( return (await basic_group_options_schema(domain, handler)).extend(
@ -177,26 +184,32 @@ CONFIG_FLOW = {
), ),
"cover": SchemaFlowFormStep( "cover": SchemaFlowFormStep(
basic_group_config_schema("cover"), basic_group_config_schema("cover"),
preview="group",
validate_user_input=set_group_type("cover"), validate_user_input=set_group_type("cover"),
), ),
"event": SchemaFlowFormStep( "event": SchemaFlowFormStep(
basic_group_config_schema("event"), basic_group_config_schema("event"),
preview="group",
validate_user_input=set_group_type("event"), validate_user_input=set_group_type("event"),
), ),
"fan": SchemaFlowFormStep( "fan": SchemaFlowFormStep(
basic_group_config_schema("fan"), basic_group_config_schema("fan"),
preview="group",
validate_user_input=set_group_type("fan"), validate_user_input=set_group_type("fan"),
), ),
"light": SchemaFlowFormStep( "light": SchemaFlowFormStep(
basic_group_config_schema("light"), basic_group_config_schema("light"),
preview="group",
validate_user_input=set_group_type("light"), validate_user_input=set_group_type("light"),
), ),
"lock": SchemaFlowFormStep( "lock": SchemaFlowFormStep(
basic_group_config_schema("lock"), basic_group_config_schema("lock"),
preview="group",
validate_user_input=set_group_type("lock"), validate_user_input=set_group_type("lock"),
), ),
"media_player": SchemaFlowFormStep( "media_player": SchemaFlowFormStep(
basic_group_config_schema("media_player"), basic_group_config_schema("media_player"),
preview="group",
validate_user_input=set_group_type("media_player"), validate_user_input=set_group_type("media_player"),
), ),
"sensor": SchemaFlowFormStep( "sensor": SchemaFlowFormStep(
@ -206,6 +219,7 @@ CONFIG_FLOW = {
), ),
"switch": SchemaFlowFormStep( "switch": SchemaFlowFormStep(
basic_group_config_schema("switch"), basic_group_config_schema("switch"),
preview="group",
validate_user_input=set_group_type("switch"), validate_user_input=set_group_type("switch"),
), ),
} }
@ -217,11 +231,26 @@ OPTIONS_FLOW = {
binary_sensor_options_schema, binary_sensor_options_schema,
preview="group", preview="group",
), ),
"cover": SchemaFlowFormStep(partial(basic_group_options_schema, "cover")), "cover": SchemaFlowFormStep(
"event": SchemaFlowFormStep(partial(basic_group_options_schema, "event")), partial(basic_group_options_schema, "cover"),
"fan": SchemaFlowFormStep(partial(basic_group_options_schema, "fan")), preview="group",
"light": SchemaFlowFormStep(partial(light_switch_options_schema, "light")), ),
"lock": SchemaFlowFormStep(partial(basic_group_options_schema, "lock")), "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( "media_player": SchemaFlowFormStep(
partial(basic_group_options_schema, "media_player"), partial(basic_group_options_schema, "media_player"),
preview="group", preview="group",
@ -230,17 +259,27 @@ OPTIONS_FLOW = {
partial(sensor_options_schema, "sensor"), partial(sensor_options_schema, "sensor"),
preview="group", 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] = {} PREVIEW_OPTIONS_SCHEMA: dict[str, vol.Schema] = {}
CREATE_PREVIEW_ENTITY: dict[ CREATE_PREVIEW_ENTITY: dict[
str, str,
Callable[[str, dict[str, Any]], GroupEntity], Callable[[str, dict[str, Any]], GroupEntity | MediaPlayerGroup],
] = { ] = {
"binary_sensor": async_create_preview_binary_sensor, "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, "sensor": async_create_preview_sensor,
"switch": async_create_preview_switch,
} }

View file

@ -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): class CoverGroup(GroupEntity, CoverEntity):
"""Representation of a CoverGroup.""" """Representation of a CoverGroup."""

View file

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import itertools import itertools
from typing import Any
import voluptuous as vol 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): class EventGroup(GroupEntity, EventEntity):
"""Representation of an event group.""" """Representation of an event group."""

View file

@ -96,6 +96,16 @@ async def async_setup_entry(
async_add_entities([FanGroup(config_entry.entry_id, config_entry.title, entities)]) 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): class FanGroup(GroupEntity, FanEntity):
"""Representation of a FanGroup.""" """Representation of a FanGroup."""

View file

@ -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( FORWARDED_ATTRIBUTES = frozenset(
{ {
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,

View file

@ -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): class LockGroup(GroupEntity, LockEntity):
"""Representation of a lock group.""" """Representation of a lock group."""

View file

@ -1,7 +1,7 @@
"""Platform allowing several media players to be grouped into one media player.""" """Platform allowing several media players to be grouped into one media player."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Callable, Mapping
from contextlib import suppress from contextlib import suppress
from typing import Any from typing import Any
@ -44,7 +44,7 @@ from homeassistant.const import (
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, 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 import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import ( 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): class MediaPlayerGroup(MediaPlayerEntity):
"""Representation of a Media Group.""" """Representation of a Media Group."""
@ -139,7 +151,8 @@ class MediaPlayerGroup(MediaPlayerEntity):
self.async_update_supported_features( self.async_update_supported_features(
event.data["entity_id"], event.data["new_state"] event.data["entity_id"], event.data["new_state"]
) )
self.async_update_state() self.async_update_group_state()
self.async_write_ha_state()
@callback @callback
def async_update_supported_features( def async_update_supported_features(
@ -208,6 +221,26 @@ class MediaPlayerGroup(MediaPlayerEntity):
else: else:
self._features[KEY_ENQUEUE].discard(entity_id) 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: async def async_added_to_hass(self) -> None:
"""Register listeners.""" """Register listeners."""
for entity_id in self._entities: for entity_id in self._entities:
@ -216,7 +249,8 @@ class MediaPlayerGroup(MediaPlayerEntity):
async_track_state_change_event( async_track_state_change_event(
self.hass, self._entities, self.async_on_state_change self.hass, self._entities, self.async_on_state_change
) )
self.async_update_state() self.async_update_group_state()
self.async_write_ha_state()
@property @property
def name(self) -> str: def name(self) -> str:
@ -391,7 +425,7 @@ class MediaPlayerGroup(MediaPlayerEntity):
await self.async_set_volume_level(max(0, volume_level - 0.1)) await self.async_set_volume_level(max(0, volume_level - 0.1))
@callback @callback
def async_update_state(self) -> None: def async_update_group_state(self) -> None:
"""Query all members and determine the media group state.""" """Query all members and determine the media group state."""
states = [ states = [
state.state state.state
@ -455,4 +489,3 @@ class MediaPlayerGroup(MediaPlayerEntity):
supported_features |= MediaPlayerEntityFeature.MEDIA_ENQUEUE supported_features |= MediaPlayerEntityFeature.MEDIA_ENQUEUE
self._attr_supported_features = supported_features self._attr_supported_features = supported_features
self.async_write_ha_state()

View file

@ -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): class SwitchGroup(GroupEntity, SwitchEntity):
"""Representation of a switch group.""" """Representation of a switch group."""

View file

@ -466,17 +466,34 @@ async def test_options_flow_hides_members(
assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by 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( @pytest.mark.parametrize(
("domain", "extra_user_input", "input_states", "group_state", "extra_attributes"), ("domain", "extra_user_input", "input_states", "group_state", "extra_attributes"),
[ [
("binary_sensor", {"all": True}, ["on", "off"], "off", [{}, {}]), ("binary_sensor", {"all": True}, ["on", "off"], "off", [{}, {}]),
( ("cover", {}, ["open", "closed"], "open", COVER_ATTRS),
"sensor", ("event", {}, ["", ""], "unknown", EVENT_ATTRS),
{"type": "max"}, ("fan", {}, ["on", "off"], "on", FAN_ATTRS),
["10", "20"], ("light", {}, ["on", "off"], "on", LIGHT_ATTRS),
"20.0", ("lock", {}, ["unlocked", "locked"], "unlocked", LOCK_ATTRS),
[{"icon": "mdi:calculator"}, {"max_entity_id": "sensor.input_two"}], ("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( async def test_config_flow_preview(
@ -553,15 +570,22 @@ async def test_config_flow_preview(
"extra_attributes", "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", "sensor",
{"type": "min"}, {"type": "min"},
{"type": "max"}, {"type": "max"},
["10", "20"], ["10", "20"],
"20.0", "20.0",
{"icon": "mdi:calculator", "max_entity_id": "sensor.input_two"}, SENSOR_ATTRS,
), ),
("switch", {}, {}, ["on", "off"], "on", [{}, {}]),
], ],
) )
async def test_option_flow_preview( async def test_option_flow_preview(
@ -575,8 +599,6 @@ async def test_option_flow_preview(
extra_attributes: dict[str, Any], extra_attributes: dict[str, Any],
) -> None: ) -> None:
"""Test the option flow preview.""" """Test the option flow preview."""
client = await hass_ws_client(hass)
input_entities = [f"{domain}.input_one", f"{domain}.input_two"] input_entities = [f"{domain}.input_one", f"{domain}.input_two"]
# Setup the config entry # 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) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
client = await hass_ws_client(hass)
result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["errors"] is None assert result["errors"] is None
@ -619,7 +643,8 @@ async def test_option_flow_preview(
msg = await client.receive_json() msg = await client.receive_json()
assert msg["event"] == { assert msg["event"] == {
"attributes": {"entity_id": input_entities, "friendly_name": "My group"} "attributes": {"entity_id": input_entities, "friendly_name": "My group"}
| extra_attributes, | extra_attributes[0]
| extra_attributes[1],
"state": group_state, "state": group_state,
} }
assert len(hass.states.async_all()) == 3 assert len(hass.states.async_all()) == 3