From e010f0af50225e040d3703ecb227fe32fd1cfdb5 Mon Sep 17 00:00:00 2001 From: samuelspagl Date: Thu, 7 Sep 2023 11:57:56 +0200 Subject: [PATCH] various improvements: - image entity updates as expected - add select options to device --- CHANGELOG.md | 0 .../samsung_soundbar/__init__.py | 2 +- .../api_extension/SoundbarDevice.py | 59 ++++++- custom_components/samsung_soundbar/image.py | 17 +- .../samsung_soundbar/media_player.py | 5 + custom_components/samsung_soundbar/number.py | 28 ++- custom_components/samsung_soundbar/select.py | 163 ++++++++++++++++++ 7 files changed, 260 insertions(+), 14 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 custom_components/samsung_soundbar/select.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/custom_components/samsung_soundbar/__init__.py b/custom_components/samsung_soundbar/__init__.py index 234eaf1..31c5c48 100644 --- a/custom_components/samsung_soundbar/__init__.py +++ b/custom_components/samsung_soundbar/__init__.py @@ -18,7 +18,7 @@ from .models import DeviceConfig, SoundbarConfig _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["media_player", "switch", "image", "number"] +PLATFORMS = ["media_player", "switch", "image", "number", "select"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/custom_components/samsung_soundbar/api_extension/SoundbarDevice.py b/custom_components/samsung_soundbar/api_extension/SoundbarDevice.py index 6b79b9e..b2fa6c8 100644 --- a/custom_components/samsung_soundbar/api_extension/SoundbarDevice.py +++ b/custom_components/samsung_soundbar/api_extension/SoundbarDevice.py @@ -1,9 +1,13 @@ +import asyncio +import datetime import json import time from urllib.parse import quote - +import logging from pysmartthings import DeviceEntity +from ..const import DOMAIN +log = logging.getLogger(__name__) class SoundbarDevice: def __init__( @@ -33,6 +37,7 @@ class SoundbarDevice: self.__media_title = "" self.__media_artist = "" self.__media_cover_url = "" + self.__media_cover_url_update_time: datetime.datetime | None = None self.__old_media_title = "" self.__max_volume = max_volume @@ -55,13 +60,24 @@ class SoundbarDevice: ] if self.__media_title != self.__old_media_title: self.__old_media_title = self.__media_title + self.__media_cover_url_update_time = datetime.datetime.now() self.__media_cover_url = await self.get_song_title_artwork( self.__media_artist, self.__media_title ) async def _update_soundmode(self): await self.update_execution_data(["/sec/networkaudio/soundmode"]) + await asyncio.sleep(0.1) payload = await self.get_execute_status() + retry = 0 + while("x.com.samsung.networkaudio.supportedSoundmode" not in payload and retry < 10): + await asyncio.sleep(0.2) + payload = await self.get_execute_status() + retry += 1 + if retry == 10: + log.error(f"[{DOMAIN}] Error: _update_soundmode exceeded a retry counter of 10") + return + self.__supported_soundmodes = payload[ "x.com.samsung.networkaudio.supportedSoundmode" ] @@ -69,13 +85,31 @@ class SoundbarDevice: async def _update_woofer(self): await self.update_execution_data(["/sec/networkaudio/woofer"]) + await asyncio.sleep(0.1) payload = await self.get_execute_status() + retry = 0 + while("x.com.samsung.networkaudio.woofer" not in payload and retry < 10): + await asyncio.sleep(0.2) + payload = await self.get_execute_status() + retry += 1 + if retry == 10: + log.error(f"[{DOMAIN}] Error: _update_woofer exceeded a retry counter of 10") + return self.__woofer_level = payload["x.com.samsung.networkaudio.woofer"] self.__woofer_connection = payload["x.com.samsung.networkaudio.connection"] async def _update_equalizer(self): await self.update_execution_data(["/sec/networkaudio/eq"]) + await asyncio.sleep(0.1) payload = await self.get_execute_status() + retry = 0 + while("x.com.samsung.networkaudio.EQname" not in payload and retry < 10): + await asyncio.sleep(0.2) + payload = await self.get_execute_status() + retry += 1 + if retry == 10: + log.error(f"[{DOMAIN}] Error: _update_equalizer exceeded a retry counter of 10") + return self.__active_eq_preset = payload["x.com.samsung.networkaudio.EQname"] self.__supported_eq_presets = payload[ "x.com.samsung.networkaudio.supportedList" @@ -85,7 +119,18 @@ class SoundbarDevice: async def _update_advanced_audio(self): await self.update_execution_data(["/sec/networkaudio/advancedaudio"]) + await asyncio.sleep(0.1) + payload = await self.get_execute_status() + retry = 0 + while("x.com.samsung.networkaudio.nightmode" not in payload and retry < 10): + await asyncio.sleep(0.2) + payload = await self.get_execute_status() + retry += 1 + if retry == 10: + log.error(f"[{DOMAIN}] Error: _update_advanced_audio exceeded a retry counter of 10") + return + self.__night_mode = payload["x.com.samsung.networkaudio.nightmode"] self.__bass_mode = payload["x.com.samsung.networkaudio.bassboost"] self.__voice_amplifier = payload["x.com.samsung.networkaudio.voiceamplifier"] @@ -132,7 +177,10 @@ class SoundbarDevice: @property def volume_level(self) -> float: - return ((self.device.status.volume / 100) * self.__max_volume) / 100 + vol = self.device.status.volume + if vol > self.__max_volume: + return 1.0 + return self.device.status.volume / self.__max_volume @property def volume_muted(self) -> bool: @@ -144,7 +192,7 @@ class SoundbarDevice: This respects the max volume and hovers between :param volume: between 0 and 1 """ - await self.device.set_volume(int(volume * self.__max_volume)) + await self.device.set_volume(int(volume * self.__max_volume), True) async def mute_volume(self, mute: bool): if mute: @@ -174,6 +222,7 @@ class SoundbarDevice: property="x.com.samsung.networkaudio.woofer", value=level, ) + self.__woofer_level = level # ------------ INPUT SOURCE ------------- @@ -304,6 +353,10 @@ class SoundbarDevice: return detail_status.value return None + @property + def media_coverart_updated(self) -> datetime.datetime: + return self.__media_cover_url_update_time + # ------------ SUPPORT FUNCTIONS ------------ async def update_execution_data(self, argument: str): diff --git a/custom_components/samsung_soundbar/image.py b/custom_components/samsung_soundbar/image.py index b40a687..c06d321 100644 --- a/custom_components/samsung_soundbar/image.py +++ b/custom_components/samsung_soundbar/image.py @@ -1,8 +1,10 @@ import logging +from datetime import datetime from homeassistant.components.image import ImageEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import UndefinedType from .models import DeviceConfig from .api_extension.SoundbarDevice import SoundbarDevice @@ -41,9 +43,22 @@ class SoundbarImageEntity(ImageEntity): sw_version=self.__device.firmware_version, ) - self._attr_image_url = self.__device.media_coverart_url + self.__updated = None # ---------- GENERAL --------------- + @property + def image_url(self) -> str | None | UndefinedType: + """Return URL of image.""" + return self.__device.media_coverart_url + + @property + def image_last_updated(self) -> datetime | None: + """The time when the image was last updated.""" + current = self.__device.media_coverart_updated + if self.__updated != current: + self._cached_image = None + self.__updated = current + return current @property def name(self): diff --git a/custom_components/samsung_soundbar/media_player.py b/custom_components/samsung_soundbar/media_player.py index 7aeb13d..40a7840 100644 --- a/custom_components/samsung_soundbar/media_player.py +++ b/custom_components/samsung_soundbar/media_player.py @@ -1,4 +1,5 @@ import logging +from typing import Mapping, Any from homeassistant.components.media_player import ( DEVICE_CLASS_SPEAKER, @@ -189,3 +190,7 @@ class SmartThingsSoundbarMediaPlayer(MediaPlayerEntity): async def async_media_stop(self): await self.device.media_stop() + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + return self.device.retrieve_data diff --git a/custom_components/samsung_soundbar/number.py b/custom_components/samsung_soundbar/number.py index 3b880e0..ee08016 100644 --- a/custom_components/samsung_soundbar/number.py +++ b/custom_components/samsung_soundbar/number.py @@ -1,6 +1,6 @@ import logging -from homeassistant.components.number import NumberEntity +from homeassistant.components.number import NumberEntity, NumberEntityDescription, NumberMode from homeassistant.helpers.entity import DeviceInfo from .models import DeviceConfig @@ -22,9 +22,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): SoundbarNumberEntity( device, "woofer_level", - device.woofer_level, + lambda: device.woofer_level, device.set_woofer, (-6, 12), + unit="dB", + mode=NumberMode.BOX ) ) async_add_entities(entities) @@ -39,9 +41,19 @@ class SoundbarNumberEntity(NumberEntity): state_function, on_function, min_max: tuple, + *, + unit: str = "%", + step_size: float = 1, + mode: NumberMode = NumberMode.SLIDER ): self.entity_id = f"number.{device.device_name}_{append_unique_id}" - + self.entity_description = NumberEntityDescription(native_max_value=min_max[1], + native_min_value=min_max[0], + mode=mode, + native_step=step_size, + native_unit_of_measurement=unit, + key=append_unique_id, + ) self.__device = device self._attr_unique_id = f"{device.device_id}_sw_{append_unique_id}" self._attr_device_info = DeviceInfo( @@ -51,6 +63,7 @@ class SoundbarNumberEntity(NumberEntity): model=self.__device.model, sw_version=self.__device.firmware_version, ) + self.__append_unique_id = append_unique_id self.__current_value_function = state_function self.__set_value_function = on_function @@ -61,17 +74,14 @@ class SoundbarNumberEntity(NumberEntity): @property def name(self): - return self.__device.device_name + return self.__append_unique_id # ------ STATE FUNCTIONS -------- @property def native_value(self) -> float | None: - return self.__current_value_function + _LOGGER.info(f"[{DOMAIN}] Soundbar Woofer number value {self.__current_value_function()}") + return self.__current_value_function() async def async_set_native_value(self, value: float): - if value > self.__max_value: - value = self.__min_value - if value < self.__min_value: - value = self.__min_value await self.__set_value_function(value) diff --git a/custom_components/samsung_soundbar/select.py b/custom_components/samsung_soundbar/select.py new file mode 100644 index 0000000..6dfe5f0 --- /dev/null +++ b/custom_components/samsung_soundbar/select.py @@ -0,0 +1,163 @@ +import logging + +from homeassistant.components.number import NumberEntity, NumberEntityDescription, NumberMode +from homeassistant.components.select import SelectEntityDescription, SelectEntity +from homeassistant.helpers.entity import DeviceInfo + +from .models import DeviceConfig +from .api_extension.SoundbarDevice import SoundbarDevice +from .const import CONF_ENTRY_DEVICE_ID, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + domain_data = hass.data[DOMAIN] + entities = [] + for key in domain_data.devices: + device_config: DeviceConfig = domain_data.devices[key] + device = device_config.device + if device.device_id == config_entry.data.get(CONF_ENTRY_DEVICE_ID): + entities.append( + EqPresetSelectEntity( + device, + "eq_preset", + ) + ) + entities.append( + SoundModeSelectEntity( + device, + "sound_mode_preset", + ) + ) + entities.append( + InputSelectEntity( + device, + "input_preset", + ) + ) + async_add_entities(entities) + return True + + +class EqPresetSelectEntity(SelectEntity): + def __init__( + self, + device: SoundbarDevice, + append_unique_id: str, + ): + self.entity_id = f"number.{device.device_name}_{append_unique_id}" + self.entity_description = SelectEntityDescription(key=append_unique_id, + ) + self.__device = device + self._attr_unique_id = f"{device.device_id}_sw_{append_unique_id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.__device.device_id)}, + name=self.__device.device_name, + manufacturer=self.__device.manufacturer, + model=self.__device.model, + sw_version=self.__device.firmware_version, + ) + self.__append_unique_id = append_unique_id + + self._attr_options = self.__device.supported_equalizer_presets + + # ---------- GENERAL --------------- + + @property + def name(self): + return self.__append_unique_id + + # ------ STATE FUNCTIONS -------- + + @property + def current_option(self) -> str | None: + """Get the current status of the select entity from device_status.""" + return self.__device.active_equalizer_preset + + async def async_select_option(self, option: str) -> None: + """Set the option.""" + + await self.__device.set_equalizer_preset(option) + + +class SoundModeSelectEntity(SelectEntity): + def __init__( + self, + device: SoundbarDevice, + append_unique_id: str, + ): + self.entity_id = f"number.{device.device_name}_{append_unique_id}" + self.entity_description = SelectEntityDescription(key=append_unique_id, + ) + self.__device = device + self._attr_unique_id = f"{device.device_id}_sw_{append_unique_id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.__device.device_id)}, + name=self.__device.device_name, + manufacturer=self.__device.manufacturer, + model=self.__device.model, + sw_version=self.__device.firmware_version, + ) + self.__append_unique_id = append_unique_id + + self._attr_options = self.__device.supported_soundmodes + + # ---------- GENERAL --------------- + + @property + def name(self): + return self.__append_unique_id + + # ------ STATE FUNCTIONS -------- + + @property + def current_option(self) -> str | None: + """Get the current status of the select entity from device_status.""" + return self.__device.sound_mode + + async def async_select_option(self, option: str) -> None: + """Set the option.""" + + await self.__device.select_sound_mode(option) + + +class InputSelectEntity(SelectEntity): + def __init__( + self, + device: SoundbarDevice, + append_unique_id: str, + ): + self.entity_id = f"number.{device.device_name}_{append_unique_id}" + self.entity_description = SelectEntityDescription(key=append_unique_id, + ) + self.__device = device + self._attr_unique_id = f"{device.device_id}_sw_{append_unique_id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.__device.device_id)}, + name=self.__device.device_name, + manufacturer=self.__device.manufacturer, + model=self.__device.model, + sw_version=self.__device.firmware_version, + ) + self.__append_unique_id = append_unique_id + + self._attr_options = self.__device.supported_input_sources + + # ---------- GENERAL --------------- + + @property + def name(self): + return self.__append_unique_id + + # ------ STATE FUNCTIONS -------- + + @property + def current_option(self) -> str | None: + """Get the current status of the select entity from device_status.""" + return self.__device.input_source + + async def async_select_option(self, option: str) -> None: + """Set the option.""" + + await self.__device.select_source(option)