Add all option to light group (#68447)

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
pull/68512/head
Jeff Rescignano 2022-03-22 11:09:18 -04:00 committed by GitHub
parent 6a66b4dbff
commit 0720b0f891
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 124 additions and 48 deletions

View File

@ -46,10 +46,20 @@ BINARY_SENSOR_OPTIONS_SCHEMA = basic_group_options_schema("binary_sensor").exten
}
)
LIGHT_OPTIONS_SCHEMA = basic_group_options_schema("light").extend(
{
vol.Required(CONF_ALL, default=False): selector.selector({"boolean": {}}),
}
)
BINARY_SENSOR_CONFIG_SCHEMA = vol.Schema(
{vol.Required("name"): selector.selector({"text": {}})}
).extend(BINARY_SENSOR_OPTIONS_SCHEMA.schema)
LIGHT_CONFIG_SCHEMA = vol.Schema(
{vol.Required("name"): selector.selector({"text": {}})}
).extend(LIGHT_OPTIONS_SCHEMA.schema)
INITIAL_STEP_SCHEMA = vol.Schema(
{
@ -81,7 +91,7 @@ CONFIG_FLOW = {
"binary_sensor": HelperFlowStep(BINARY_SENSOR_CONFIG_SCHEMA),
"cover": HelperFlowStep(basic_group_config_schema("cover")),
"fan": HelperFlowStep(basic_group_config_schema("fan")),
"light": HelperFlowStep(basic_group_config_schema("light")),
"light": HelperFlowStep(LIGHT_CONFIG_SCHEMA),
"media_player": HelperFlowStep(basic_group_config_schema("media_player")),
}
@ -91,7 +101,7 @@ OPTIONS_FLOW = {
"binary_sensor": HelperFlowStep(BINARY_SENSOR_OPTIONS_SCHEMA),
"cover": HelperFlowStep(basic_group_options_schema("cover")),
"fan": HelperFlowStep(basic_group_options_schema("fan")),
"light": HelperFlowStep(basic_group_options_schema("light")),
"light": HelperFlowStep(LIGHT_OPTIONS_SCHEMA),
"media_player": HelperFlowStep(basic_group_options_schema("media_player")),
}

View File

@ -47,6 +47,7 @@ from homeassistant.const import (
SERVICE_TURN_ON,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import Event, HomeAssistant, State, callback
from homeassistant.helpers import config_validation as cv, entity_registry as er
@ -58,6 +59,7 @@ from . import GroupEntity
from .util import find_state_attributes, mean_tuple, reduce_attribute
DEFAULT_NAME = "Light Group"
CONF_ALL = "all"
# No limit on parallel updates to enable a group calling another group
PARALLEL_UPDATES = 0
@ -67,6 +69,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Required(CONF_ENTITIES): cv.entities_domain(light.DOMAIN),
vol.Optional(CONF_ALL): cv.boolean,
}
)
@ -87,7 +90,10 @@ async def async_setup_platform(
async_add_entities(
[
LightGroup(
config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES]
config.get(CONF_UNIQUE_ID),
config[CONF_NAME],
config[CONF_ENTITIES],
config.get(CONF_ALL),
)
]
)
@ -103,9 +109,10 @@ async def async_setup_entry(
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
)
mode = config_entry.options[CONF_ALL]
async_add_entities(
[LightGroup(config_entry.entry_id, config_entry.title, entities)]
[LightGroup(config_entry.entry_id, config_entry.title, entities, mode)]
)
@ -132,12 +139,13 @@ class LightGroup(GroupEntity, LightEntity):
_attr_available = False
_attr_icon = "mdi:lightbulb-group"
_attr_is_on = False
_attr_max_mireds = 500
_attr_min_mireds = 154
_attr_should_poll = False
def __init__(self, unique_id: str | None, name: str, entity_ids: list[str]) -> None:
def __init__(
self, unique_id: str | None, name: str, entity_ids: list[str], mode: str | None
) -> None:
"""Initialize a light group."""
self._entity_ids = entity_ids
self._white_value: int | None = None
@ -145,6 +153,9 @@ class LightGroup(GroupEntity, LightEntity):
self._attr_name = name
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
self._attr_unique_id = unique_id
self.mode = any
if mode:
self.mode = all
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@ -207,7 +218,22 @@ class LightGroup(GroupEntity, LightEntity):
states: list[State] = list(filter(None, all_states))
on_states = [state for state in states if state.state == STATE_ON]
self._attr_is_on = len(on_states) > 0
# filtered_states are members currently in the state machine
filtered_states: list[str] = [x.state for x in all_states if x is not None]
valid_state = self.mode(
state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in filtered_states
)
if not valid_state:
# Set as unknown if any / all member is unknown or unavailable
self._attr_is_on = None
else:
# Set as ON if any / all member is ON
self._attr_is_on = self.mode(
list(map(lambda x: x == STATE_ON, filtered_states))
)
self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states)
self._attr_brightness = reduce_attribute(on_states, ATTR_BRIGHTNESS)

View File

@ -32,7 +32,9 @@
}
},
"light": {
"description": "[%key:component::group::config::step::binary_sensor::description%]",
"data": {
"all": "[%key:component::group::config::step::binary_sensor::data::all%]",
"title": "[%key:component::group::config::step::user::title%]",
"entities": "[%key:component::group::config::step::binary_sensor::data::entities%]",
"name": "[%key:component::group::config::step::binary_sensor::data::name%]"
@ -67,6 +69,7 @@
},
"light_options": {
"data": {
"all": "[%key:component::group::config::step::binary_sensor::data::all%]",
"entities": "[%key:component::group::config::step::binary_sensor::data::entities%]"
}
},

View File

@ -15,62 +15,30 @@
"entities": "Members",
"name": "Name",
"title": "New Group"
},
"description": "Select group options"
},
"cover_options": {
"data": {
"entities": "Group members"
},
"description": "Select group options"
}
},
"fan": {
"data": {
"entities": "Members",
"name": "Name",
"title": "New Group"
},
"description": "Select group options"
},
"fan_options": {
"data": {
"entities": "Group members"
},
"description": "Select group options"
},
"init": {
"data": {
"group_type": "Group type"
},
"description": "Select group type"
}
},
"light": {
"data": {
"all": "All entities",
"entities": "Members",
"name": "Name",
"title": "New Group"
},
"description": "Select group options"
},
"light_options": {
"data": {
"entities": "Group members"
},
"description": "Select group options"
"description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on."
},
"media_player": {
"data": {
"entities": "Members",
"name": "Name",
"title": "New Group"
},
"description": "Select group options"
},
"media_player_options": {
"data": {
"entities": "Group members"
},
"description": "Select group options"
}
},
"user": {
"data": {
@ -100,6 +68,7 @@
},
"light_options": {
"data": {
"all": "All entities",
"entities": "Members"
}
},

View File

@ -19,7 +19,8 @@ from tests.common import MockConfigEntry
("binary_sensor", "on", "on", {}, {"all": True}, {"all": True}, {}),
("cover", "open", "open", {}, {}, {}, {}),
("fan", "on", "on", {}, {}, {}, {}),
("light", "on", "on", {}, {}, {}, {}),
("light", "on", "on", {}, {}, {"all": False}, {}),
("light", "on", "on", {}, {"all": True}, {"all": True}, {}),
("media_player", "on", "on", {}, {}, {}, {}),
),
)
@ -174,7 +175,7 @@ def get_suggested(schema, key):
("binary_sensor", "on", {"all": False}),
("cover", "open", {}),
("fan", "on", {}),
("light", "on", {}),
("light", "on", {"all": False}),
("media_player", "on", {}),
),
)

View File

@ -1370,7 +1370,7 @@ async def test_plant_group(hass):
("binary_sensor", "on", {"all": False}),
("cover", "open", {}),
("fan", "on", {}),
("light", "on", {}),
("light", "on", {"all": False}),
("media_player", "on", {}),
),
)
@ -1435,7 +1435,7 @@ async def test_setup_and_remove_config_entry(
("binary_sensor", {"all": False}),
("cover", {}),
("fan", {}),
("light", {}),
("light", {"all": False}),
("media_player", {}),
),
)

View File

@ -49,6 +49,7 @@ from homeassistant.const import (
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
@ -68,6 +69,7 @@ async def test_default_state(hass):
"entities": ["light.kitchen", "light.bedroom"],
"name": "Bedroom Group",
"unique_id": "unique_identifier",
"all": "false",
}
},
)
@ -102,6 +104,7 @@ async def test_state_reporting(hass):
LIGHT_DOMAIN: {
"platform": DOMAIN,
"entities": ["light.test1", "light.test2"],
"all": "false",
}
},
)
@ -130,6 +133,49 @@ async def test_state_reporting(hass):
assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE
async def test_state_reporting_all(hass):
"""Test the state reporting."""
await async_setup_component(
hass,
LIGHT_DOMAIN,
{
LIGHT_DOMAIN: {
"platform": DOMAIN,
"entities": ["light.test1", "light.test2"],
"all": "true",
}
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
hass.states.async_set("light.test1", STATE_ON)
hass.states.async_set("light.test2", STATE_UNAVAILABLE)
await hass.async_block_till_done()
assert hass.states.get("light.light_group").state == STATE_UNKNOWN
hass.states.async_set("light.test1", STATE_ON)
hass.states.async_set("light.test2", STATE_OFF)
await hass.async_block_till_done()
assert hass.states.get("light.light_group").state == STATE_OFF
hass.states.async_set("light.test1", STATE_OFF)
hass.states.async_set("light.test2", STATE_OFF)
await hass.async_block_till_done()
assert hass.states.get("light.light_group").state == STATE_OFF
hass.states.async_set("light.test1", STATE_ON)
hass.states.async_set("light.test2", STATE_ON)
await hass.async_block_till_done()
assert hass.states.get("light.light_group").state == STATE_ON
hass.states.async_set("light.test1", STATE_UNAVAILABLE)
hass.states.async_set("light.test2", STATE_UNAVAILABLE)
await hass.async_block_till_done()
assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE
async def test_brightness(hass, enable_custom_integrations):
"""Test brightness reporting."""
platform = getattr(hass.components, "test.light")
@ -155,6 +201,7 @@ async def test_brightness(hass, enable_custom_integrations):
{
"platform": DOMAIN,
"entities": ["light.test1", "light.test2"],
"all": "false",
},
]
},
@ -225,6 +272,7 @@ async def test_color_hs(hass, enable_custom_integrations):
{
"platform": DOMAIN,
"entities": ["light.test1", "light.test2"],
"all": "false",
},
]
},
@ -296,6 +344,7 @@ async def test_color_rgb(hass, enable_custom_integrations):
{
"platform": DOMAIN,
"entities": ["light.test1", "light.test2"],
"all": "false",
},
]
},
@ -367,6 +416,7 @@ async def test_color_rgbw(hass, enable_custom_integrations):
{
"platform": DOMAIN,
"entities": ["light.test1", "light.test2"],
"all": "false",
},
]
},
@ -438,6 +488,7 @@ async def test_color_rgbww(hass, enable_custom_integrations):
{
"platform": DOMAIN,
"entities": ["light.test1", "light.test2"],
"all": "false",
},
]
},
@ -489,6 +540,7 @@ async def test_white_value(hass):
LIGHT_DOMAIN: {
"platform": DOMAIN,
"entities": ["light.test1", "light.test2"],
"all": "false",
}
},
)
@ -548,6 +600,7 @@ async def test_white(hass, enable_custom_integrations):
{
"platform": DOMAIN,
"entities": ["light.test1", "light.test2"],
"all": "false",
},
]
},
@ -603,6 +656,7 @@ async def test_color_temp(hass, enable_custom_integrations):
{
"platform": DOMAIN,
"entities": ["light.test1", "light.test2"],
"all": "false",
},
]
},
@ -674,6 +728,7 @@ async def test_emulated_color_temp_group(hass, enable_custom_integrations):
{
"platform": DOMAIN,
"entities": ["light.test1", "light.test2", "light.test3"],
"all": "false",
},
]
},
@ -739,6 +794,7 @@ async def test_min_max_mireds(hass, enable_custom_integrations):
{
"platform": DOMAIN,
"entities": ["light.test1", "light.test2"],
"all": "false",
},
]
},
@ -784,6 +840,7 @@ async def test_effect_list(hass):
LIGHT_DOMAIN: {
"platform": DOMAIN,
"entities": ["light.test1", "light.test2"],
"all": "false",
}
},
)
@ -843,6 +900,7 @@ async def test_effect(hass):
LIGHT_DOMAIN: {
"platform": DOMAIN,
"entities": ["light.test1", "light.test2", "light.test3"],
"all": "false",
}
},
)
@ -909,6 +967,7 @@ async def test_supported_color_modes(hass, enable_custom_integrations):
{
"platform": DOMAIN,
"entities": ["light.test1", "light.test2", "light.test3"],
"all": "false",
},
]
},
@ -957,6 +1016,7 @@ async def test_color_mode(hass, enable_custom_integrations):
{
"platform": DOMAIN,
"entities": ["light.test1", "light.test2", "light.test3"],
"all": "false",
},
]
},
@ -1051,6 +1111,7 @@ async def test_color_mode2(hass, enable_custom_integrations):
"light.test5",
"light.test6",
],
"all": "false",
},
]
},
@ -1082,6 +1143,7 @@ async def test_supported_features(hass):
LIGHT_DOMAIN: {
"platform": DOMAIN,
"entities": ["light.test1", "light.test2"],
"all": "false",
}
},
)
@ -1157,6 +1219,7 @@ async def test_service_calls(hass, enable_custom_integrations, supported_color_m
"light.ceiling_lights",
"light.kitchen_lights",
],
"all": "false",
},
]
},
@ -1269,6 +1332,7 @@ async def test_service_call_effect(hass):
"light.ceiling_lights",
"light.kitchen_lights",
],
"all": "false",
},
]
},
@ -1369,6 +1433,7 @@ async def test_reload(hass):
"light.ceiling_lights",
"light.kitchen_lights",
],
"all": "false",
},
]
},
@ -1481,11 +1546,13 @@ async def test_nested_group(hass):
"platform": DOMAIN,
"entities": ["light.bedroom_group"],
"name": "Nested Group",
"all": "false",
},
{
"platform": DOMAIN,
"entities": ["light.bed_light", "light.kitchen_lights"],
"name": "Bedroom Group",
"all": "false",
},
]
},