Add Elgato power-on behavior select (#180721)

This commit is contained in:
Franck Nijhof
2026-08-30 11:58:40 +02:00
committed by GitHub
parent 9b368640e4
commit af6ddab93a
6 changed files with 285 additions and 1 deletions
+7 -1
View File
@@ -10,7 +10,13 @@ from .coordinator import ElgatoConfigEntry, ElgatoDataUpdateCoordinator
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [Platform.BUTTON, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH]
PLATFORMS = [
Platform.BUTTON,
Platform.LIGHT,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -1,5 +1,10 @@
{
"entity": {
"select": {
"power_on_behavior": {
"default": "mdi:power-settings"
}
},
"switch": {
"bypass": {
"default": "mdi:battery-off-outline"
+100
View File
@@ -0,0 +1,100 @@
"""Support for Elgato select entities."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any, override
from elgato import Elgato, PowerOnBehavior
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ElgatoConfigEntry, ElgatoData, ElgatoDataUpdateCoordinator
from .entity import ElgatoEntity
from .helpers import elgato_exception_handler
PARALLEL_UPDATES = 1
# The device also has a value 0, which it reports when it has no opinion yet.
# It cannot be selected, so it is not an option, and it leaves the entity
# unknown until the behavior is actually set.
POWER_ON_BEHAVIORS = {
PowerOnBehavior.RESTORE_LAST: "restore_last",
PowerOnBehavior.USE_DEFAULTS: "use_defaults",
}
POWER_ON_BEHAVIOR_OPTIONS = {value: key for key, value in POWER_ON_BEHAVIORS.items()}
@dataclass(frozen=True, kw_only=True)
class ElgatoSelectEntityDescription(SelectEntityDescription):
"""Class describing Elgato select entities."""
has_fn: Callable[[ElgatoData], bool] = lambda _: True
current_fn: Callable[[ElgatoData], str | None]
select_fn: Callable[[Elgato, str], Awaitable[Any]]
SELECTS = [
ElgatoSelectEntityDescription(
key="power_on_behavior",
translation_key="power_on_behavior",
entity_category=EntityCategory.CONFIG,
options=list(POWER_ON_BEHAVIORS.values()),
current_fn=lambda x: POWER_ON_BEHAVIORS.get(x.settings.power_on_behavior),
select_fn=lambda client, option: client.power_on_behavior(
behavior=POWER_ON_BEHAVIOR_OPTIONS[option]
),
),
]
async def async_setup_entry(
hass: HomeAssistant,
entry: ElgatoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Elgato select entities based on a config entry."""
coordinator = entry.runtime_data
async_add_entities(
ElgatoSelectEntity(
coordinator=coordinator,
description=description,
)
for description in SELECTS
if description.has_fn(coordinator.data)
)
class ElgatoSelectEntity(ElgatoEntity, SelectEntity):
"""Representation of an Elgato select entity."""
entity_description: ElgatoSelectEntityDescription
def __init__(
self,
coordinator: ElgatoDataUpdateCoordinator,
description: ElgatoSelectEntityDescription,
) -> None:
"""Initiate Elgato select entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = (
f"{coordinator.data.info.serial_number}_{description.key}"
)
@property
@override
def current_option(self) -> str | None:
"""Return the selected option."""
return self.entity_description.current_fn(self.coordinator.data)
@elgato_exception_handler
@override
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await self.entity_description.select_fn(self.coordinator.client, option)
await self.coordinator.async_request_refresh()
@@ -36,6 +36,15 @@
}
},
"entity": {
"select": {
"power_on_behavior": {
"name": "Power-on behavior",
"state": {
"restore_last": "Restore last state",
"use_defaults": "Use defaults"
}
}
},
"sensor": {
"charge_power": {
"name": "Charging power"
@@ -0,0 +1,94 @@
# serializer version: 1
# name: test_power_on_behavior[key-light]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Frenck Power-on behavior',
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'restore_last',
'use_defaults',
]),
}),
'context': <ANY>,
'entity_id': 'select.frenck_power_on_behavior',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'restore_last',
})
# ---
# name: test_power_on_behavior[key-light].1
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SelectEntityCapabilityAttribute.OPTIONS: 'options'>: list([
'restore_last',
'use_defaults',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.frenck_power_on_behavior',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Power-on behavior',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Power-on behavior',
'platform': 'elgato',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'power_on_behavior',
'unique_id': 'CN11A1A00001_power_on_behavior',
'unit_of_measurement': None,
})
# ---
# name: test_power_on_behavior[key-light].2
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'aa:bb:cc:dd:ee:ff',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': '53',
'id': <ANY>,
'identifiers': set({
tuple(
'elgato',
'CN11A1A00001',
),
}),
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light',
'model_id': None,
'name': 'Frenck',
'name_by_user': None,
'serial_number': 'CN11A1A00001',
'sw_version': '1.0.3 (192)',
'via_device_id': None,
})
# ---
+70
View File
@@ -0,0 +1,70 @@
"""Tests for the Elgato select platform."""
from unittest.mock import MagicMock
from elgato import ElgatoError, PowerOnBehavior
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.select import (
ATTR_OPTION,
DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
pytestmark = [
pytest.mark.parametrize("device_fixtures", ["key-light"]),
pytest.mark.usefixtures("device_fixtures", "init_integration"),
]
async def test_power_on_behavior(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
mock_elgato: MagicMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test the Elgato power-on behavior select."""
entity_id = "select.frenck_power_on_behavior"
assert (state := hass.states.get(entity_id))
assert state == snapshot
assert (entry := entity_registry.async_get(entity_id))
assert entry == snapshot
assert entry.device_id
assert (device_entry := device_registry.async_get(entry.device_id))
assert device_entry == snapshot
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "use_defaults"},
blocking=True,
)
assert len(mock_elgato.power_on_behavior.mock_calls) == 1
mock_elgato.power_on_behavior.assert_called_once_with(
behavior=PowerOnBehavior.USE_DEFAULTS
)
mock_elgato.power_on_behavior.side_effect = ElgatoError
with pytest.raises(
HomeAssistantError,
match="An unknown error occurred while communicating with the Elgato device",
):
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "restore_last"},
blocking=True,
)
assert len(mock_elgato.power_on_behavior.mock_calls) == 2