From 34220200c10bc35d85c68d0c091634cf2cf80dbf Mon Sep 17 00:00:00 2001 From: Matrix Date: Wed, 7 Feb 2024 20:42:33 +0800 Subject: [PATCH] Fix YoLink SpeakerHub support (#107925) * improve * Fix when hub offline/online message pushing * fix as suggestion * check config entry load state * Add exception translation --- homeassistant/components/yolink/__init__.py | 14 ++++++++++++-- homeassistant/components/yolink/number.py | 18 ++++++++++++++++-- homeassistant/components/yolink/services.py | 16 ++++++++++++++-- homeassistant/components/yolink/services.yaml | 2 -- homeassistant/components/yolink/strings.json | 5 +++++ 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 01395fd5f5f..270bd550038 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -19,8 +19,10 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, + config_validation as cv, device_registry as dr, ) +from homeassistant.helpers.typing import ConfigType from . import api from .const import DOMAIN, YOLINK_EVENT @@ -30,6 +32,8 @@ from .services import async_register_services SCAN_INTERVAL = timedelta(minutes=5) +CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) + PLATFORMS = [ Platform.BINARY_SENSOR, @@ -96,6 +100,14 @@ class YoLinkHomeStore: device_coordinators: dict[str, YoLinkCoordinator] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up YoLink.""" + + async_register_services(hass) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up yolink from a config entry.""" hass.data.setdefault(DOMAIN, {}) @@ -147,8 +159,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - async_register_services(hass, entry) - async def async_yolink_unload(event) -> None: """Unload yolink.""" await yolink_home.async_unload() diff --git a/homeassistant/components/yolink/number.py b/homeassistant/components/yolink/number.py index 1ec20cd4d17..a7ba89e1f6c 100644 --- a/homeassistant/components/yolink/number.py +++ b/homeassistant/components/yolink/number.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import Any from yolink.client_request import ClientRequest from yolink.const import ATTR_DEVICE_SPEAKER_HUB @@ -30,6 +31,7 @@ class YoLinkNumberTypeConfigEntityDescription(NumberEntityDescription): """YoLink NumberEntity description.""" exists_fn: Callable[[YoLinkDevice], bool] + should_update_entity: Callable value: Callable @@ -37,6 +39,14 @@ NUMBER_TYPE_CONF_SUPPORT_DEVICES = [ATTR_DEVICE_SPEAKER_HUB] SUPPORT_SET_VOLUME_DEVICES = [ATTR_DEVICE_SPEAKER_HUB] + +def get_volume_value(state: dict[str, Any]) -> int | None: + """Get volume option.""" + if (options := state.get("options")) is not None: + return options.get("volume") + return None + + DEVICE_CONFIG_DESCRIPTIONS: tuple[YoLinkNumberTypeConfigEntityDescription, ...] = ( YoLinkNumberTypeConfigEntityDescription( key=OPTIONS_VALUME, @@ -48,7 +58,8 @@ DEVICE_CONFIG_DESCRIPTIONS: tuple[YoLinkNumberTypeConfigEntityDescription, ...] native_unit_of_measurement=None, icon="mdi:volume-high", exists_fn=lambda device: device.device_type in SUPPORT_SET_VOLUME_DEVICES, - value=lambda state: state["options"]["volume"], + should_update_entity=lambda value: value is not None, + value=get_volume_value, ), ) @@ -98,7 +109,10 @@ class YoLinkNumberTypeConfigEntity(YoLinkEntity, NumberEntity): @callback def update_entity_state(self, state: dict) -> None: """Update HA Entity State.""" - attr_val = self.entity_description.value(state) + if ( + attr_val := self.entity_description.value(state) + ) is None and self.entity_description.should_update_entity(attr_val) is False: + return self._attr_native_value = attr_val self.async_write_ha_state() diff --git a/homeassistant/components/yolink/services.py b/homeassistant/components/yolink/services.py index bb2c660ef56..e41e3dce260 100644 --- a/homeassistant/components/yolink/services.py +++ b/homeassistant/components/yolink/services.py @@ -3,8 +3,9 @@ import voluptuous as vol from yolink.client_request import ClientRequest -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv, device_registry as dr from .const import ( @@ -19,7 +20,7 @@ from .const import ( SERVICE_PLAY_ON_SPEAKER_HUB = "play_on_speaker_hub" -def async_register_services(hass: HomeAssistant, entry: ConfigEntry) -> None: +def async_register_services(hass: HomeAssistant) -> None: """Register services for YoLink integration.""" async def handle_speaker_hub_play_call(service_call: ServiceCall) -> None: @@ -28,6 +29,17 @@ def async_register_services(hass: HomeAssistant, entry: ConfigEntry) -> None: device_registry = dr.async_get(hass) device_entry = device_registry.async_get(service_data[ATTR_TARGET_DEVICE]) if device_entry is not None: + for entry_id in device_entry.config_entries: + if (entry := hass.config_entries.async_get_entry(entry_id)) is None: + continue + if entry.domain == DOMAIN: + break + if entry is None or entry.state == ConfigEntryState.NOT_LOADED: + raise ServiceValidationError( + "Config entry not found or not loaded!", + translation_domain=DOMAIN, + translation_key="invalid_config_entry", + ) home_store = hass.data[DOMAIN][entry.entry_id] for identifier in device_entry.identifiers: if ( diff --git a/homeassistant/components/yolink/services.yaml b/homeassistant/components/yolink/services.yaml index 939eba3e7f5..5f7a3ec3122 100644 --- a/homeassistant/components/yolink/services.yaml +++ b/homeassistant/components/yolink/services.yaml @@ -7,9 +7,7 @@ play_on_speaker_hub: device: filter: - integration: yolink - manufacturer: YoLink model: SpeakerHub - message: required: true example: hello, yolink diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json index 9661abe096c..83e712328f9 100644 --- a/homeassistant/components/yolink/strings.json +++ b/homeassistant/components/yolink/strings.json @@ -37,6 +37,11 @@ "button_4_long_press": "Button_4 (long press)" } }, + "exceptions": { + "invalid_config_entry": { + "message": "Config entry not found or not loaded!" + } + }, "entity": { "switch": { "usb_ports": { "name": "USB ports" },