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 .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,
}

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):
"""Representation of a CoverGroup."""

View file

@ -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."""

View file

@ -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."""

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(
{
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):
"""Representation of a lock group."""

View file

@ -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()

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):
"""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
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