forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add button group support (home-assistant#121715)
Co-authored-by: G Johansson <[email protected]>
- Loading branch information
1 parent
acb4a92
commit f94b28f
Showing
5 changed files
with
286 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
"""Platform allowing several button entities to be grouped into one single button.""" | ||
|
||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
import voluptuous as vol | ||
|
||
from homeassistant.components.button import ( | ||
DOMAIN, | ||
PLATFORM_SCHEMA as BUTTON_PLATFORM_SCHEMA, | ||
SERVICE_PRESS, | ||
ButtonEntity, | ||
) | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import ( | ||
ATTR_ENTITY_ID, | ||
CONF_ENTITIES, | ||
CONF_NAME, | ||
CONF_UNIQUE_ID, | ||
STATE_UNAVAILABLE, | ||
) | ||
from homeassistant.core import HomeAssistant, callback | ||
from homeassistant.helpers import config_validation as cv, entity_registry as er | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType | ||
|
||
from .entity import GroupEntity | ||
|
||
DEFAULT_NAME = "Button group" | ||
|
||
# No limit on parallel updates to enable a group calling another group | ||
PARALLEL_UPDATES = 0 | ||
|
||
PLATFORM_SCHEMA = BUTTON_PLATFORM_SCHEMA.extend( | ||
{ | ||
vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), | ||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, | ||
vol.Optional(CONF_UNIQUE_ID): cv.string, | ||
} | ||
) | ||
|
||
|
||
async def async_setup_platform( | ||
_: HomeAssistant, | ||
config: ConfigType, | ||
async_add_entities: AddEntitiesCallback, | ||
__: DiscoveryInfoType | None = None, | ||
) -> None: | ||
"""Set up the button group platform.""" | ||
async_add_entities( | ||
[ | ||
ButtonGroup( | ||
config.get(CONF_UNIQUE_ID), | ||
config[CONF_NAME], | ||
config[CONF_ENTITIES], | ||
) | ||
] | ||
) | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, | ||
config_entry: ConfigEntry, | ||
async_add_entities: AddEntitiesCallback, | ||
) -> None: | ||
"""Initialize button group config entry.""" | ||
registry = er.async_get(hass) | ||
entities = er.async_validate_entity_ids( | ||
registry, config_entry.options[CONF_ENTITIES] | ||
) | ||
async_add_entities( | ||
[ | ||
ButtonGroup( | ||
config_entry.entry_id, | ||
config_entry.title, | ||
entities, | ||
) | ||
] | ||
) | ||
|
||
|
||
@callback | ||
def async_create_preview_button( | ||
hass: HomeAssistant, name: str, validated_config: dict[str, Any] | ||
) -> ButtonGroup: | ||
"""Create a preview button.""" | ||
return ButtonGroup( | ||
None, | ||
name, | ||
validated_config[CONF_ENTITIES], | ||
) | ||
|
||
|
||
class ButtonGroup(GroupEntity, ButtonEntity): | ||
"""Representation of an button group.""" | ||
|
||
_attr_available = False | ||
_attr_should_poll = False | ||
|
||
def __init__( | ||
self, | ||
unique_id: str | None, | ||
name: str, | ||
entity_ids: list[str], | ||
) -> None: | ||
"""Initialize a button group.""" | ||
self._entity_ids = entity_ids | ||
self._attr_name = name | ||
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} | ||
self._attr_unique_id = unique_id | ||
|
||
async def async_press(self) -> None: | ||
"""Forward the press to all buttons in the group.""" | ||
await self.hass.services.async_call( | ||
DOMAIN, | ||
SERVICE_PRESS, | ||
{ATTR_ENTITY_ID: self._entity_ids}, | ||
blocking=True, | ||
context=self._context, | ||
) | ||
|
||
@callback | ||
def async_update_group_state(self) -> None: | ||
"""Query all members and determine the button group state.""" | ||
# Set group as unavailable if all members are unavailable or missing | ||
self._attr_available = any( | ||
state.state != STATE_UNAVAILABLE | ||
for entity_id in self._entity_ids | ||
if (state := self.hass.states.get(entity_id)) is not None | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
"""The tests for the group button platform.""" | ||
|
||
from freezegun.api import FrozenDateTimeFactory | ||
import pytest | ||
|
||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS | ||
from homeassistant.components.group import DOMAIN | ||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers import entity_registry as er | ||
from homeassistant.setup import async_setup_component | ||
from homeassistant.util import dt as dt_util | ||
|
||
|
||
async def test_default_state( | ||
hass: HomeAssistant, entity_registry: er.EntityRegistry | ||
) -> None: | ||
"""Test button group default state.""" | ||
hass.states.async_set("button.notify_light", "2021-01-01T23:59:59.123+00:00") | ||
await async_setup_component( | ||
hass, | ||
BUTTON_DOMAIN, | ||
{ | ||
BUTTON_DOMAIN: { | ||
"platform": DOMAIN, | ||
"entities": ["button.notify_light", "button.self_destruct"], | ||
"name": "Button group", | ||
"unique_id": "unique_identifier", | ||
} | ||
}, | ||
) | ||
await hass.async_block_till_done() | ||
await hass.async_start() | ||
await hass.async_block_till_done() | ||
|
||
state = hass.states.get("button.button_group") | ||
assert state is not None | ||
assert state.state == STATE_UNKNOWN | ||
assert state.attributes.get(ATTR_ENTITY_ID) == [ | ||
"button.notify_light", | ||
"button.self_destruct", | ||
] | ||
|
||
entry = entity_registry.async_get("button.button_group") | ||
assert entry | ||
assert entry.unique_id == "unique_identifier" | ||
|
||
|
||
async def test_state_reporting(hass: HomeAssistant) -> None: | ||
"""Test the state reporting. | ||
The group state is unavailable if all group members are unavailable. | ||
Otherwise, the group state represents the last time the grouped button was pressed. | ||
""" | ||
await async_setup_component( | ||
hass, | ||
BUTTON_DOMAIN, | ||
{ | ||
BUTTON_DOMAIN: { | ||
"platform": DOMAIN, | ||
"entities": ["button.test1", "button.test2"], | ||
} | ||
}, | ||
) | ||
await hass.async_block_till_done() | ||
await hass.async_start() | ||
await hass.async_block_till_done() | ||
|
||
# Initial state with no group member in the state machine -> unavailable | ||
assert hass.states.get("button.button_group").state == STATE_UNAVAILABLE | ||
|
||
# All group members unavailable -> unavailable | ||
hass.states.async_set("button.test1", STATE_UNAVAILABLE) | ||
hass.states.async_set("button.test2", STATE_UNAVAILABLE) | ||
await hass.async_block_till_done() | ||
assert hass.states.get("button.button_group").state == STATE_UNAVAILABLE | ||
|
||
# All group members available, but no group member pressed -> unknown | ||
hass.states.async_set("button.test1", "2021-01-01T23:59:59.123+00:00") | ||
hass.states.async_set("button.test2", "2022-02-02T23:59:59.123+00:00") | ||
await hass.async_block_till_done() | ||
assert hass.states.get("button.button_group").state == STATE_UNKNOWN | ||
|
||
|
||
@pytest.mark.usefixtures("enable_custom_integrations") | ||
async def test_service_calls( | ||
hass: HomeAssistant, freezer: FrozenDateTimeFactory | ||
) -> None: | ||
"""Test service calls.""" | ||
await async_setup_component( | ||
hass, | ||
BUTTON_DOMAIN, | ||
{ | ||
BUTTON_DOMAIN: [ | ||
{"platform": "demo"}, | ||
{ | ||
"platform": DOMAIN, | ||
"entities": [ | ||
"button.push", | ||
"button.self_destruct", | ||
], | ||
}, | ||
] | ||
}, | ||
) | ||
await hass.async_block_till_done() | ||
|
||
assert hass.states.get("button.button_group").state == STATE_UNKNOWN | ||
assert hass.states.get("button.push").state == STATE_UNKNOWN | ||
|
||
now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") | ||
freezer.move_to(now) | ||
|
||
await hass.services.async_call( | ||
BUTTON_DOMAIN, | ||
SERVICE_PRESS, | ||
{ATTR_ENTITY_ID: "button.button_group"}, | ||
blocking=True, | ||
) | ||
|
||
assert hass.states.get("button.button_group").state == now.isoformat() | ||
assert hass.states.get("button.push").state == now.isoformat() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters