Coordinator refactor in Elgato (#87490)

pull/87498/head
Franck Nijhof 2023-02-05 21:54:30 +01:00 committed by GitHub
parent 0aa489e3f0
commit d389de71f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 111 additions and 126 deletions

View File

@ -1,59 +1,20 @@
"""Support for Elgato Lights.""" """Support for Elgato Lights."""
from typing import NamedTuple
from elgato import Elgato, ElgatoConnectionError, Info, State
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_INTERVAL from .const import DOMAIN
from .coordinator import ElgatoDataUpdateCoordinator
PLATFORMS = [Platform.BUTTON, Platform.LIGHT] PLATFORMS = [Platform.BUTTON, Platform.LIGHT]
class HomeAssistantElgatoData(NamedTuple):
"""Elgato data stored in the Home Assistant data object."""
coordinator: DataUpdateCoordinator[State]
client: Elgato
info: Info
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Elgato Light from a config entry.""" """Set up Elgato Light from a config entry."""
session = async_get_clientsession(hass) coordinator = ElgatoDataUpdateCoordinator(hass, entry)
elgato = Elgato(
entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
session=session,
)
async def _async_update_data() -> State:
"""Fetch Elgato data."""
try:
return await elgato.state()
except ElgatoConnectionError as err:
raise UpdateFailed(err) from err
coordinator: DataUpdateCoordinator[State] = DataUpdateCoordinator(
hass,
LOGGER,
name=f"{DOMAIN}_{entry.data[CONF_HOST]}",
update_interval=SCAN_INTERVAL,
update_method=_async_update_data,
)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
info = await elgato.info() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantElgatoData(
client=elgato,
coordinator=coordinator,
info=info,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
@ -62,8 +23,5 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Elgato Light config entry.""" """Unload Elgato Light config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
# Cleanup
del hass.data[DOMAIN][entry.entry_id] del hass.data[DOMAIN][entry.entry_id]
if not hass.data[DOMAIN]:
del hass.data[DOMAIN]
return unload_ok return unload_ok

View File

@ -1,24 +1,19 @@
"""Support for Elgato button.""" """Support for Elgato button."""
from __future__ import annotations from __future__ import annotations
import logging from elgato import ElgatoError
from elgato import Elgato, ElgatoError, Info
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantElgatoData
from .const import DOMAIN from .const import DOMAIN
from .coordinator import ElgatoDataUpdateCoordinator
from .entity import ElgatoEntity from .entity import ElgatoEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
@ -26,30 +21,30 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up Elgato button based on a config entry.""" """Set up Elgato button based on a config entry."""
data: HomeAssistantElgatoData = hass.data[DOMAIN][entry.entry_id] coordinator: ElgatoDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities( async_add_entities([ElgatoIdentifyButton(coordinator)])
[ElgatoIdentifyButton(data.client, data.info, entry.data.get(CONF_MAC))]
)
class ElgatoIdentifyButton(ElgatoEntity, ButtonEntity): class ElgatoIdentifyButton(ElgatoEntity, ButtonEntity):
"""Defines an Elgato identify button.""" """Defines an Elgato identify button."""
def __init__(self, client: Elgato, info: Info, mac: str | None) -> None: def __init__(self, coordinator: ElgatoDataUpdateCoordinator) -> None:
"""Initialize the button entity.""" """Initialize the button entity."""
super().__init__(client, info, mac) super().__init__(coordinator=coordinator)
self.entity_description = ButtonEntityDescription( self.entity_description = ButtonEntityDescription(
key="identify", key="identify",
name="Identify", name="Identify",
icon="mdi:help", icon="mdi:help",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
) )
self._attr_unique_id = f"{info.serial_number}_{self.entity_description.key}" self._attr_unique_id = (
f"{coordinator.data.info.serial_number}_{self.entity_description.key}"
)
async def async_press(self) -> None: async def async_press(self) -> None:
"""Identify the light, will make it blink.""" """Identify the light, will make it blink."""
try: try:
await self.client.identify() await self.coordinator.client.identify()
except ElgatoError as error: except ElgatoError as error:
raise HomeAssistantError( raise HomeAssistantError(
"An error occurred while identifying the Elgato Light" "An error occurred while identifying the Elgato Light"

View File

@ -0,0 +1,53 @@
"""DataUpdateCoordinator for Elgato."""
from dataclasses import dataclass
from elgato import Elgato, ElgatoConnectionError, Info, Settings, State
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
@dataclass
class ElgatoData:
"""Elgato data stored in the DataUpdateCoordinator."""
info: Info
settings: Settings
state: State
class ElgatoDataUpdateCoordinator(DataUpdateCoordinator[ElgatoData]):
"""Class to manage fetching Elgato data."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the coordinator."""
self.config_entry = entry
self.client = Elgato(
entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
session=async_get_clientsession(hass),
)
super().__init__(
hass,
LOGGER,
name=f"{DOMAIN}_{entry.data[CONF_HOST]}",
update_interval=SCAN_INTERVAL,
)
async def _async_update_data(self) -> ElgatoData:
"""Fetch data from the Elgato device."""
try:
return ElgatoData(
info=await self.client.info(),
settings=await self.client.settings(),
state=await self.client.state(),
)
except ElgatoConnectionError as err:
raise UpdateFailed(err) from err

View File

@ -6,16 +6,16 @@ from typing import Any
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import HomeAssistantElgatoData
from .const import DOMAIN from .const import DOMAIN
from .coordinator import ElgatoDataUpdateCoordinator
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
data: HomeAssistantElgatoData = hass.data[DOMAIN][entry.entry_id] coordinator: ElgatoDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
return { return {
"info": data.info.dict(), "info": coordinator.data.info.dict(),
"state": data.coordinator.data.dict(), "state": coordinator.data.state.dict(),
} }

View File

@ -1,31 +1,32 @@
"""Base entity for the Elgato integration.""" """Base entity for the Elgato integration."""
from __future__ import annotations from __future__ import annotations
from elgato import Elgato, Info from homeassistant.const import CONF_MAC
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN from .const import DOMAIN
from .coordinator import ElgatoDataUpdateCoordinator
class ElgatoEntity(Entity): class ElgatoEntity(CoordinatorEntity[ElgatoDataUpdateCoordinator]):
"""Defines an Elgato entity.""" """Defines an Elgato entity."""
_attr_has_entity_name = True _attr_has_entity_name = True
def __init__(self, client: Elgato, info: Info, mac: str | None) -> None: def __init__(self, coordinator: ElgatoDataUpdateCoordinator) -> None:
"""Initialize an Elgato entity.""" """Initialize an Elgato entity."""
self.client = client super().__init__(coordinator=coordinator)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, info.serial_number)}, identifiers={(DOMAIN, coordinator.data.info.serial_number)},
manufacturer="Elgato", manufacturer="Elgato",
model=info.product_name, model=coordinator.data.info.product_name,
name=info.display_name, name=coordinator.data.info.display_name,
sw_version=f"{info.firmware_version} ({info.firmware_build_number})", sw_version=f"{coordinator.data.info.firmware_version} ({coordinator.data.info.firmware_build_number})",
hw_version=str(info.hardware_board_type), hw_version=str(coordinator.data.info.hardware_board_type),
) )
if mac is not None: if (mac := coordinator.config_entry.data.get(CONF_MAC)) is not None:
self._attr_device_info["connections"] = { self._attr_device_info["connections"] = {
(CONNECTION_NETWORK_MAC, format_mac(mac)) (CONNECTION_NETWORK_MAC, format_mac(mac))
} }

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from elgato import Elgato, ElgatoError, Info, Settings, State from elgato import ElgatoError
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
@ -13,20 +13,15 @@ from homeassistant.components.light import (
LightEntity, LightEntity,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import ( from homeassistant.helpers.entity_platform import (
AddEntitiesCallback, AddEntitiesCallback,
async_get_current_platform, async_get_current_platform,
) )
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from . import HomeAssistantElgatoData
from .const import DOMAIN, SERVICE_IDENTIFY from .const import DOMAIN, SERVICE_IDENTIFY
from .coordinator import ElgatoDataUpdateCoordinator
from .entity import ElgatoEntity from .entity import ElgatoEntity
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@ -38,20 +33,8 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up Elgato Light based on a config entry.""" """Set up Elgato Light based on a config entry."""
data: HomeAssistantElgatoData = hass.data[DOMAIN][entry.entry_id] coordinator: ElgatoDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
settings = await data.client.settings() async_add_entities([ElgatoLight(coordinator)])
async_add_entities(
[
ElgatoLight(
data.coordinator,
data.client,
data.info,
entry.data.get(CONF_MAC),
settings,
)
],
True,
)
platform = async_get_current_platform() platform = async_get_current_platform()
platform.async_register_entity_service( platform.async_register_entity_service(
@ -61,30 +44,20 @@ async def async_setup_entry(
) )
class ElgatoLight( class ElgatoLight(ElgatoEntity, LightEntity):
ElgatoEntity, CoordinatorEntity[DataUpdateCoordinator[State]], LightEntity
):
"""Defines an Elgato Light.""" """Defines an Elgato Light."""
def __init__( _attr_min_mireds = 143
self, _attr_max_mireds = 344
coordinator: DataUpdateCoordinator[State],
client: Elgato,
info: Info,
mac: str | None,
settings: Settings,
) -> None:
"""Initialize Elgato Light."""
super().__init__(client, info, mac)
CoordinatorEntity.__init__(self, coordinator)
self._attr_min_mireds = 143 def __init__(self, coordinator: ElgatoDataUpdateCoordinator) -> None:
self._attr_max_mireds = 344 """Initialize Elgato Light."""
super().__init__(coordinator)
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP} self._attr_supported_color_modes = {ColorMode.COLOR_TEMP}
self._attr_unique_id = info.serial_number self._attr_unique_id = coordinator.data.info.serial_number
# Elgato Light supporting color, have a different temperature range # Elgato Light supporting color, have a different temperature range
if settings.power_on_hue is not None: if self.coordinator.data.settings.power_on_hue is not None:
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
self._attr_min_mireds = 153 self._attr_min_mireds = 153
self._attr_max_mireds = 285 self._attr_max_mireds = 285
@ -92,17 +65,17 @@ class ElgatoLight(
@property @property
def brightness(self) -> int | None: def brightness(self) -> int | None:
"""Return the brightness of this light between 1..255.""" """Return the brightness of this light between 1..255."""
return round((self.coordinator.data.brightness * 255) / 100) return round((self.coordinator.data.state.brightness * 255) / 100)
@property @property
def color_temp(self) -> int | None: def color_temp(self) -> int | None:
"""Return the CT color value in mireds.""" """Return the CT color value in mireds."""
return self.coordinator.data.temperature return self.coordinator.data.state.temperature
@property @property
def color_mode(self) -> str | None: def color_mode(self) -> str | None:
"""Return the color mode of the light.""" """Return the color mode of the light."""
if self.coordinator.data.hue is not None: if self.coordinator.data.state.hue is not None:
return ColorMode.HS return ColorMode.HS
return ColorMode.COLOR_TEMP return ColorMode.COLOR_TEMP
@ -110,17 +83,20 @@ class ElgatoLight(
@property @property
def hs_color(self) -> tuple[float, float] | None: def hs_color(self) -> tuple[float, float] | None:
"""Return the hue and saturation color value [float, float].""" """Return the hue and saturation color value [float, float]."""
return (self.coordinator.data.hue or 0, self.coordinator.data.saturation or 0) return (
self.coordinator.data.state.hue or 0,
self.coordinator.data.state.saturation or 0,
)
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the state of the light.""" """Return the state of the light."""
return self.coordinator.data.on return self.coordinator.data.state.on
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light.""" """Turn off the light."""
try: try:
await self.client.light(on=False) await self.coordinator.client.light(on=False)
except ElgatoError as error: except ElgatoError as error:
raise HomeAssistantError( raise HomeAssistantError(
"An error occurred while updating the Elgato Light" "An error occurred while updating the Elgato Light"
@ -155,7 +131,7 @@ class ElgatoLight(
temperature = self.color_temp temperature = self.color_temp
try: try:
await self.client.light( await self.coordinator.client.light(
on=True, on=True,
brightness=brightness, brightness=brightness,
hue=hue, hue=hue,
@ -172,7 +148,7 @@ class ElgatoLight(
async def async_identify(self) -> None: async def async_identify(self) -> None:
"""Identify the light, will make it blink.""" """Identify the light, will make it blink."""
try: try:
await self.client.identify() await self.coordinator.client.identify()
except ElgatoError as error: except ElgatoError as error:
raise HomeAssistantError( raise HomeAssistantError(
"An error occurred while identifying the Elgato Light" "An error occurred while identifying the Elgato Light"

View File

@ -65,7 +65,9 @@ def mock_elgato(request: pytest.FixtureRequest) -> Generator[None, MagicMock, No
if hasattr(request, "param") and request.param: if hasattr(request, "param") and request.param:
variant = request.param variant = request.param
with patch("homeassistant.components.elgato.Elgato", autospec=True) as elgato_mock: with patch(
"homeassistant.components.elgato.coordinator.Elgato", autospec=True
) as elgato_mock:
elgato = elgato_mock.return_value elgato = elgato_mock.return_value
elgato.info.return_value = Info.parse_raw(load_fixture("info.json", DOMAIN)) elgato.info.return_value = Info.parse_raw(load_fixture("info.json", DOMAIN))
elgato.state.return_value = State.parse_raw( elgato.state.return_value = State.parse_raw(