Fix yolink device unavailable on startup (#72579)

* fetch device state on startup

* Suggest change

* suggest fix

* fix

* fix

* Fix suggest

* suggest fix
This commit is contained in:
Matrix 2022-05-30 02:54:23 +08:00 committed by Paulus Schoutsen
parent 6bf6a0f7bc
commit ce4825c9e2
8 changed files with 177 additions and 161 deletions

View File

@ -1,25 +1,28 @@
"""The yolink integration.""" """The yolink integration."""
from __future__ import annotations from __future__ import annotations
import asyncio
from datetime import timedelta from datetime import timedelta
import logging
import async_timeout
from yolink.client import YoLinkClient from yolink.client import YoLinkClient
from yolink.device import YoLinkDevice
from yolink.exception import YoLinkAuthFailError, YoLinkClientError
from yolink.model import BRDP
from yolink.mqtt_client import MqttClient from yolink.mqtt_client import MqttClient
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
from . import api from . import api
from .const import ATTR_CLIENT, ATTR_COORDINATOR, ATTR_MQTT_CLIENT, DOMAIN from .const import ATTR_CLIENT, ATTR_COORDINATORS, ATTR_DEVICE, ATTR_MQTT_CLIENT, DOMAIN
from .coordinator import YoLinkCoordinator from .coordinator import YoLinkCoordinator
SCAN_INTERVAL = timedelta(minutes=5) SCAN_INTERVAL = timedelta(minutes=5)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SIREN, Platform.SWITCH] PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SIREN, Platform.SWITCH]
@ -41,18 +44,63 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
yolink_http_client = YoLinkClient(auth_mgr) yolink_http_client = YoLinkClient(auth_mgr)
yolink_mqtt_client = MqttClient(auth_mgr) yolink_mqtt_client = MqttClient(auth_mgr)
coordinator = YoLinkCoordinator(hass, yolink_http_client, yolink_mqtt_client)
await coordinator.init_coordinator() def on_message_callback(message: tuple[str, BRDP]) -> None:
data = message[1]
device_id = message[0]
if data.event is None:
return
event_param = data.event.split(".")
event_type = event_param[len(event_param) - 1]
if event_type not in (
"Report",
"Alert",
"StatusChange",
"getState",
):
return
resolved_state = data.data
if resolved_state is None:
return
entry_data = hass.data[DOMAIN].get(entry.entry_id)
if entry_data is None:
return
device_coordinators = entry_data.get(ATTR_COORDINATORS)
if device_coordinators is None:
return
device_coordinator = device_coordinators.get(device_id)
if device_coordinator is None:
return
device_coordinator.async_set_updated_data(resolved_state)
try: try:
await coordinator.async_config_entry_first_refresh() async with async_timeout.timeout(10):
except ConfigEntryNotReady as ex: device_response = await yolink_http_client.get_auth_devices()
_LOGGER.error("Fetching initial data failed: %s", ex) home_info = await yolink_http_client.get_general_info()
await yolink_mqtt_client.init_home_connection(
home_info.data["id"], on_message_callback
)
except YoLinkAuthFailError as yl_auth_err:
raise ConfigEntryAuthFailed from yl_auth_err
except (YoLinkClientError, asyncio.TimeoutError) as err:
raise ConfigEntryNotReady from err
hass.data[DOMAIN][entry.entry_id] = { hass.data[DOMAIN][entry.entry_id] = {
ATTR_CLIENT: yolink_http_client, ATTR_CLIENT: yolink_http_client,
ATTR_MQTT_CLIENT: yolink_mqtt_client, ATTR_MQTT_CLIENT: yolink_mqtt_client,
ATTR_COORDINATOR: coordinator,
} }
auth_devices = device_response.data[ATTR_DEVICE]
device_coordinators = {}
for device_info in auth_devices:
device = YoLinkDevice(device_info, yolink_http_client)
device_coordinator = YoLinkCoordinator(hass, device)
try:
await device_coordinator.async_config_entry_first_refresh()
except ConfigEntryNotReady:
# Not failure by fetching device state
device_coordinator.data = {}
device_coordinators[device.device_id] = device_coordinator
hass.data[DOMAIN][entry.entry_id][ATTR_COORDINATORS] = device_coordinators
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True return True

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any
from yolink.device import YoLinkDevice from yolink.device import YoLinkDevice
@ -16,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
ATTR_COORDINATOR, ATTR_COORDINATORS,
ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_DOOR_SENSOR,
ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_LEAK_SENSOR,
ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_MOTION_SENSOR,
@ -32,7 +33,7 @@ class YoLinkBinarySensorEntityDescription(BinarySensorEntityDescription):
exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True
state_key: str = "state" state_key: str = "state"
value: Callable[[str], bool | None] = lambda _: None value: Callable[[Any], bool | None] = lambda _: None
SENSOR_DEVICE_TYPE = [ SENSOR_DEVICE_TYPE = [
@ -47,14 +48,14 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = (
icon="mdi:door", icon="mdi:door",
device_class=BinarySensorDeviceClass.DOOR, device_class=BinarySensorDeviceClass.DOOR,
name="State", name="State",
value=lambda value: value == "open", value=lambda value: value == "open" if value is not None else None,
exists_fn=lambda device: device.device_type in [ATTR_DEVICE_DOOR_SENSOR], exists_fn=lambda device: device.device_type in [ATTR_DEVICE_DOOR_SENSOR],
), ),
YoLinkBinarySensorEntityDescription( YoLinkBinarySensorEntityDescription(
key="motion_state", key="motion_state",
device_class=BinarySensorDeviceClass.MOTION, device_class=BinarySensorDeviceClass.MOTION,
name="Motion", name="Motion",
value=lambda value: value == "alert", value=lambda value: value == "alert" if value is not None else None,
exists_fn=lambda device: device.device_type in [ATTR_DEVICE_MOTION_SENSOR], exists_fn=lambda device: device.device_type in [ATTR_DEVICE_MOTION_SENSOR],
), ),
YoLinkBinarySensorEntityDescription( YoLinkBinarySensorEntityDescription(
@ -62,7 +63,7 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = (
name="Leak", name="Leak",
icon="mdi:water", icon="mdi:water",
device_class=BinarySensorDeviceClass.MOISTURE, device_class=BinarySensorDeviceClass.MOISTURE,
value=lambda value: value == "alert", value=lambda value: value == "alert" if value is not None else None,
exists_fn=lambda device: device.device_type in [ATTR_DEVICE_LEAK_SENSOR], exists_fn=lambda device: device.device_type in [ATTR_DEVICE_LEAK_SENSOR],
), ),
) )
@ -74,18 +75,20 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up YoLink Sensor from a config entry.""" """Set up YoLink Sensor from a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS]
sensor_devices = [ binary_sensor_device_coordinators = [
device device_coordinator
for device in coordinator.yl_devices for device_coordinator in device_coordinators.values()
if device.device_type in SENSOR_DEVICE_TYPE if device_coordinator.device.device_type in SENSOR_DEVICE_TYPE
] ]
entities = [] entities = []
for sensor_device in sensor_devices: for binary_sensor_device_coordinator in binary_sensor_device_coordinators:
for description in SENSOR_TYPES: for description in SENSOR_TYPES:
if description.exists_fn(sensor_device): if description.exists_fn(binary_sensor_device_coordinator.device):
entities.append( entities.append(
YoLinkBinarySensorEntity(coordinator, description, sensor_device) YoLinkBinarySensorEntity(
binary_sensor_device_coordinator, description
)
) )
async_add_entities(entities) async_add_entities(entities)
@ -99,18 +102,21 @@ class YoLinkBinarySensorEntity(YoLinkEntity, BinarySensorEntity):
self, self,
coordinator: YoLinkCoordinator, coordinator: YoLinkCoordinator,
description: YoLinkBinarySensorEntityDescription, description: YoLinkBinarySensorEntityDescription,
device: YoLinkDevice,
) -> None: ) -> None:
"""Init YoLink Sensor.""" """Init YoLink Sensor."""
super().__init__(coordinator, device) super().__init__(coordinator)
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" self._attr_unique_id = (
self._attr_name = f"{device.device_name} ({self.entity_description.name})" f"{coordinator.device.device_id} {self.entity_description.key}"
)
self._attr_name = (
f"{coordinator.device.device_name} ({self.entity_description.name})"
)
@callback @callback
def update_entity_state(self, state: dict) -> None: def update_entity_state(self, state: dict[str, Any]) -> None:
"""Update HA Entity State.""" """Update HA Entity State."""
self._attr_is_on = self.entity_description.value( self._attr_is_on = self.entity_description.value(
state[self.entity_description.state_key] state.get(self.entity_description.state_key)
) )
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -5,7 +5,7 @@ MANUFACTURER = "YoLink"
HOME_ID = "homeId" HOME_ID = "homeId"
HOME_SUBSCRIPTION = "home_subscription" HOME_SUBSCRIPTION = "home_subscription"
ATTR_PLATFORM_SENSOR = "sensor" ATTR_PLATFORM_SENSOR = "sensor"
ATTR_COORDINATOR = "coordinator" ATTR_COORDINATORS = "coordinators"
ATTR_DEVICE = "devices" ATTR_DEVICE = "devices"
ATTR_DEVICE_TYPE = "type" ATTR_DEVICE_TYPE = "type"
ATTR_DEVICE_NAME = "name" ATTR_DEVICE_NAME = "name"

View File

@ -1,22 +1,18 @@
"""YoLink DataUpdateCoordinator.""" """YoLink DataUpdateCoordinator."""
from __future__ import annotations from __future__ import annotations
import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
import async_timeout import async_timeout
from yolink.client import YoLinkClient
from yolink.device import YoLinkDevice from yolink.device import YoLinkDevice
from yolink.exception import YoLinkAuthFailError, YoLinkClientError from yolink.exception import YoLinkAuthFailError, YoLinkClientError
from yolink.model import BRDP
from yolink.mqtt_client import MqttClient
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ATTR_DEVICE, ATTR_DEVICE_STATE, DOMAIN from .const import ATTR_DEVICE_STATE, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -24,9 +20,7 @@ _LOGGER = logging.getLogger(__name__)
class YoLinkCoordinator(DataUpdateCoordinator[dict]): class YoLinkCoordinator(DataUpdateCoordinator[dict]):
"""YoLink DataUpdateCoordinator.""" """YoLink DataUpdateCoordinator."""
def __init__( def __init__(self, hass: HomeAssistant, device: YoLinkDevice) -> None:
self, hass: HomeAssistant, yl_client: YoLinkClient, yl_mqtt_client: MqttClient
) -> None:
"""Init YoLink DataUpdateCoordinator. """Init YoLink DataUpdateCoordinator.
fetch state every 30 minutes base on yolink device heartbeat interval fetch state every 30 minutes base on yolink device heartbeat interval
@ -35,75 +29,17 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]):
super().__init__( super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=30) hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=30)
) )
self._client = yl_client self.device = device
self._mqtt_client = yl_mqtt_client
self.yl_devices: list[YoLinkDevice] = []
self.data = {}
def on_message_callback(self, message: tuple[str, BRDP]): async def _async_update_data(self) -> dict:
"""On message callback.""" """Fetch device state."""
data = message[1]
if data.event is None:
return
event_param = data.event.split(".")
event_type = event_param[len(event_param) - 1]
if event_type not in (
"Report",
"Alert",
"StatusChange",
"getState",
):
return
resolved_state = data.data
if resolved_state is None:
return
self.data[message[0]] = resolved_state
self.async_set_updated_data(self.data)
async def init_coordinator(self):
"""Init coordinator."""
try: try:
async with async_timeout.timeout(10): async with async_timeout.timeout(10):
home_info = await self._client.get_general_info() device_state_resp = await self.device.fetch_state_with_api()
await self._mqtt_client.init_home_connection(
home_info.data["id"], self.on_message_callback
)
async with async_timeout.timeout(10):
device_response = await self._client.get_auth_devices()
except YoLinkAuthFailError as yl_auth_err:
raise ConfigEntryAuthFailed from yl_auth_err
except (YoLinkClientError, asyncio.TimeoutError) as err:
raise ConfigEntryNotReady from err
yl_devices: list[YoLinkDevice] = []
for device_info in device_response.data[ATTR_DEVICE]:
yl_devices.append(YoLinkDevice(device_info, self._client))
self.yl_devices = yl_devices
async def fetch_device_state(self, device: YoLinkDevice):
"""Fetch Device State."""
try:
async with async_timeout.timeout(10):
device_state_resp = await device.fetch_state_with_api()
if ATTR_DEVICE_STATE in device_state_resp.data:
self.data[device.device_id] = device_state_resp.data[
ATTR_DEVICE_STATE
]
except YoLinkAuthFailError as yl_auth_err: except YoLinkAuthFailError as yl_auth_err:
raise ConfigEntryAuthFailed from yl_auth_err raise ConfigEntryAuthFailed from yl_auth_err
except YoLinkClientError as yl_client_err: except YoLinkClientError as yl_client_err:
raise UpdateFailed( raise UpdateFailed from yl_client_err
f"Error communicating with API: {yl_client_err}" if ATTR_DEVICE_STATE in device_state_resp.data:
) from yl_client_err return device_state_resp.data[ATTR_DEVICE_STATE]
return {}
async def _async_update_data(self) -> dict:
fetch_tasks = []
for yl_device in self.yl_devices:
fetch_tasks.append(self.fetch_device_state(yl_device))
if fetch_tasks:
await asyncio.gather(*fetch_tasks)
return self.data

View File

@ -3,8 +3,6 @@ from __future__ import annotations
from abc import abstractmethod from abc import abstractmethod
from yolink.device import YoLinkDevice
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -19,20 +17,24 @@ class YoLinkEntity(CoordinatorEntity[YoLinkCoordinator]):
def __init__( def __init__(
self, self,
coordinator: YoLinkCoordinator, coordinator: YoLinkCoordinator,
device_info: YoLinkDevice,
) -> None: ) -> None:
"""Init YoLink Entity.""" """Init YoLink Entity."""
super().__init__(coordinator) super().__init__(coordinator)
self.device = device_info
@property @property
def device_id(self) -> str: def device_id(self) -> str:
"""Return the device id of the YoLink device.""" """Return the device id of the YoLink device."""
return self.device.device_id return self.coordinator.device.device_id
async def async_added_to_hass(self) -> None:
"""Update state."""
await super().async_added_to_hass()
return self._handle_coordinator_update()
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
data = self.coordinator.data.get(self.device.device_id) """Update state."""
data = self.coordinator.data
if data is not None: if data is not None:
self.update_entity_state(data) self.update_entity_state(data)
@ -40,10 +42,10 @@ class YoLinkEntity(CoordinatorEntity[YoLinkCoordinator]):
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the device info for HA.""" """Return the device info for HA."""
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self.device.device_id)}, identifiers={(DOMAIN, self.coordinator.device.device_id)},
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
model=self.device.device_type, model=self.coordinator.device.device_type,
name=self.device.device_name, name=self.coordinator.device.device_name,
) )
@callback @callback

View File

@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import percentage from homeassistant.util import percentage
from .const import ( from .const import (
ATTR_COORDINATOR, ATTR_COORDINATORS,
ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_DOOR_SENSOR,
ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_MOTION_SENSOR,
ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_TH_SENSOR,
@ -54,7 +54,9 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
value=lambda value: percentage.ordered_list_item_to_percentage( value=lambda value: percentage.ordered_list_item_to_percentage(
[1, 2, 3, 4], value [1, 2, 3, 4], value
), )
if value is not None
else None,
exists_fn=lambda device: device.device_type exists_fn=lambda device: device.device_type
in [ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_MOTION_SENSOR], in [ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_MOTION_SENSOR],
), ),
@ -89,18 +91,21 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up YoLink Sensor from a config entry.""" """Set up YoLink Sensor from a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS]
sensor_devices = [ sensor_device_coordinators = [
device device_coordinator
for device in coordinator.yl_devices for device_coordinator in device_coordinators.values()
if device.device_type in SENSOR_DEVICE_TYPE if device_coordinator.device.device_type in SENSOR_DEVICE_TYPE
] ]
entities = [] entities = []
for sensor_device in sensor_devices: for sensor_device_coordinator in sensor_device_coordinators:
for description in SENSOR_TYPES: for description in SENSOR_TYPES:
if description.exists_fn(sensor_device): if description.exists_fn(sensor_device_coordinator.device):
entities.append( entities.append(
YoLinkSensorEntity(coordinator, description, sensor_device) YoLinkSensorEntity(
sensor_device_coordinator,
description,
)
) )
async_add_entities(entities) async_add_entities(entities)
@ -114,18 +119,21 @@ class YoLinkSensorEntity(YoLinkEntity, SensorEntity):
self, self,
coordinator: YoLinkCoordinator, coordinator: YoLinkCoordinator,
description: YoLinkSensorEntityDescription, description: YoLinkSensorEntityDescription,
device: YoLinkDevice,
) -> None: ) -> None:
"""Init YoLink Sensor.""" """Init YoLink Sensor."""
super().__init__(coordinator, device) super().__init__(coordinator)
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" self._attr_unique_id = (
self._attr_name = f"{device.device_name} ({self.entity_description.name})" f"{coordinator.device.device_id} {self.entity_description.key}"
)
self._attr_name = (
f"{coordinator.device.device_name} ({self.entity_description.name})"
)
@callback @callback
def update_entity_state(self, state: dict) -> None: def update_entity_state(self, state: dict) -> None:
"""Update HA Entity State.""" """Update HA Entity State."""
self._attr_native_value = self.entity_description.value( self._attr_native_value = self.entity_description.value(
state[self.entity_description.key] state.get(self.entity_description.key)
) )
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ATTR_COORDINATOR, ATTR_DEVICE_SIREN, DOMAIN from .const import ATTR_COORDINATORS, ATTR_DEVICE_SIREN, DOMAIN
from .coordinator import YoLinkCoordinator from .coordinator import YoLinkCoordinator
from .entity import YoLinkEntity from .entity import YoLinkEntity
@ -28,14 +28,14 @@ class YoLinkSirenEntityDescription(SirenEntityDescription):
"""YoLink SirenEntityDescription.""" """YoLink SirenEntityDescription."""
exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True
value: Callable[[str], bool | None] = lambda _: None value: Callable[[Any], bool | None] = lambda _: None
DEVICE_TYPES: tuple[YoLinkSirenEntityDescription, ...] = ( DEVICE_TYPES: tuple[YoLinkSirenEntityDescription, ...] = (
YoLinkSirenEntityDescription( YoLinkSirenEntityDescription(
key="state", key="state",
name="State", name="State",
value=lambda value: value == "alert", value=lambda value: value == "alert" if value is not None else None,
exists_fn=lambda device: device.device_type in [ATTR_DEVICE_SIREN], exists_fn=lambda device: device.device_type in [ATTR_DEVICE_SIREN],
), ),
) )
@ -49,16 +49,20 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up YoLink siren from a config entry.""" """Set up YoLink siren from a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS]
devices = [ siren_device_coordinators = [
device for device in coordinator.yl_devices if device.device_type in DEVICE_TYPE device_coordinator
for device_coordinator in device_coordinators.values()
if device_coordinator.device.device_type in DEVICE_TYPE
] ]
entities = [] entities = []
for device in devices: for siren_device_coordinator in siren_device_coordinators:
for description in DEVICE_TYPES: for description in DEVICE_TYPES:
if description.exists_fn(device): if description.exists_fn(siren_device_coordinator.device):
entities.append( entities.append(
YoLinkSirenEntity(config_entry, coordinator, description, device) YoLinkSirenEntity(
config_entry, siren_device_coordinator, description
)
) )
async_add_entities(entities) async_add_entities(entities)
@ -73,23 +77,26 @@ class YoLinkSirenEntity(YoLinkEntity, SirenEntity):
config_entry: ConfigEntry, config_entry: ConfigEntry,
coordinator: YoLinkCoordinator, coordinator: YoLinkCoordinator,
description: YoLinkSirenEntityDescription, description: YoLinkSirenEntityDescription,
device: YoLinkDevice,
) -> None: ) -> None:
"""Init YoLink Siren.""" """Init YoLink Siren."""
super().__init__(coordinator, device) super().__init__(coordinator)
self.config_entry = config_entry self.config_entry = config_entry
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" self._attr_unique_id = (
self._attr_name = f"{device.device_name} ({self.entity_description.name})" f"{coordinator.device.device_id} {self.entity_description.key}"
)
self._attr_name = (
f"{coordinator.device.device_name} ({self.entity_description.name})"
)
self._attr_supported_features = ( self._attr_supported_features = (
SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF
) )
@callback @callback
def update_entity_state(self, state: dict) -> None: def update_entity_state(self, state: dict[str, Any]) -> None:
"""Update HA Entity State.""" """Update HA Entity State."""
self._attr_is_on = self.entity_description.value( self._attr_is_on = self.entity_description.value(
state[self.entity_description.key] state.get(self.entity_description.key)
) )
self.async_write_ha_state() self.async_write_ha_state()
@ -97,7 +104,7 @@ class YoLinkSirenEntity(YoLinkEntity, SirenEntity):
"""Call setState api to change siren state.""" """Call setState api to change siren state."""
try: try:
# call_device_http_api will check result, fail by raise YoLinkClientError # call_device_http_api will check result, fail by raise YoLinkClientError
await self.device.call_device_http_api( await self.coordinator.device.call_device_http_api(
"setState", {"state": {"alarm": state}} "setState", {"state": {"alarm": state}}
) )
except YoLinkAuthFailError as yl_auth_err: except YoLinkAuthFailError as yl_auth_err:

View File

@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ATTR_COORDINATOR, ATTR_DEVICE_OUTLET, DOMAIN from .const import ATTR_COORDINATORS, ATTR_DEVICE_OUTLET, DOMAIN
from .coordinator import YoLinkCoordinator from .coordinator import YoLinkCoordinator
from .entity import YoLinkEntity from .entity import YoLinkEntity
@ -28,7 +28,7 @@ class YoLinkSwitchEntityDescription(SwitchEntityDescription):
"""YoLink SwitchEntityDescription.""" """YoLink SwitchEntityDescription."""
exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True
value: Callable[[str], bool | None] = lambda _: None value: Callable[[Any], bool | None] = lambda _: None
DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = (
@ -36,7 +36,7 @@ DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = (
key="state", key="state",
device_class=SwitchDeviceClass.OUTLET, device_class=SwitchDeviceClass.OUTLET,
name="State", name="State",
value=lambda value: value == "open", value=lambda value: value == "open" if value is not None else None,
exists_fn=lambda device: device.device_type in [ATTR_DEVICE_OUTLET], exists_fn=lambda device: device.device_type in [ATTR_DEVICE_OUTLET],
), ),
) )
@ -50,16 +50,20 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up YoLink Sensor from a config entry.""" """Set up YoLink Sensor from a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS]
devices = [ switch_device_coordinators = [
device for device in coordinator.yl_devices if device.device_type in DEVICE_TYPE device_coordinator
for device_coordinator in device_coordinators.values()
if device_coordinator.device.device_type in DEVICE_TYPE
] ]
entities = [] entities = []
for device in devices: for switch_device_coordinator in switch_device_coordinators:
for description in DEVICE_TYPES: for description in DEVICE_TYPES:
if description.exists_fn(device): if description.exists_fn(switch_device_coordinator.device):
entities.append( entities.append(
YoLinkSwitchEntity(config_entry, coordinator, description, device) YoLinkSwitchEntity(
config_entry, switch_device_coordinator, description
)
) )
async_add_entities(entities) async_add_entities(entities)
@ -74,20 +78,23 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity):
config_entry: ConfigEntry, config_entry: ConfigEntry,
coordinator: YoLinkCoordinator, coordinator: YoLinkCoordinator,
description: YoLinkSwitchEntityDescription, description: YoLinkSwitchEntityDescription,
device: YoLinkDevice,
) -> None: ) -> None:
"""Init YoLink Outlet.""" """Init YoLink Outlet."""
super().__init__(coordinator, device) super().__init__(coordinator)
self.config_entry = config_entry self.config_entry = config_entry
self.entity_description = description self.entity_description = description
self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" self._attr_unique_id = (
self._attr_name = f"{device.device_name} ({self.entity_description.name})" f"{coordinator.device.device_id} {self.entity_description.key}"
)
self._attr_name = (
f"{coordinator.device.device_name} ({self.entity_description.name})"
)
@callback @callback
def update_entity_state(self, state: dict) -> None: def update_entity_state(self, state: dict[str, Any]) -> None:
"""Update HA Entity State.""" """Update HA Entity State."""
self._attr_is_on = self.entity_description.value( self._attr_is_on = self.entity_description.value(
state[self.entity_description.key] state.get(self.entity_description.key)
) )
self.async_write_ha_state() self.async_write_ha_state()
@ -95,7 +102,9 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity):
"""Call setState api to change outlet state.""" """Call setState api to change outlet state."""
try: try:
# call_device_http_api will check result, fail by raise YoLinkClientError # call_device_http_api will check result, fail by raise YoLinkClientError
await self.device.call_device_http_api("setState", {"state": state}) await self.coordinator.device.call_device_http_api(
"setState", {"state": state}
)
except YoLinkAuthFailError as yl_auth_err: except YoLinkAuthFailError as yl_auth_err:
self.config_entry.async_start_reauth(self.hass) self.config_entry.async_start_reauth(self.hass)
raise HomeAssistantError(yl_auth_err) from yl_auth_err raise HomeAssistantError(yl_auth_err) from yl_auth_err