✨ various improvements (#5)
- Added various improvements and new features. - First version of a documentation --------- Co-authored-by: samuelspagl <samuel@spagl-media.de>
This commit is contained in:
		
							parent
							
								
									b7ff6d1eb0
								
							
						
					
					
						commit
						f93019dd68
					
				|  | @ -40,8 +40,9 @@ are not documented... ;) | |||
|   - bass level | ||||
|   - *[to come] equalizer bands* | ||||
| - `select` entity | ||||
|   - *[to come] sound mode* (additional control in the "Device" tab) | ||||
|   - *[to come] equalizer preset* | ||||
|   - sound mode (additional control in the "Device" tab) | ||||
|   - input (additional control in the "Device" tab) | ||||
|   - equalizer preset | ||||
| 
 | ||||
| ## How to install it: | ||||
| 
 | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -19,29 +19,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities): | |||
|         device = device_config.device | ||||
|         if device.device_id == config_entry.data.get(CONF_ENTRY_DEVICE_ID): | ||||
|             entities.append( | ||||
|                 SoundbarNumberEntity( | ||||
|                 SoundbarWooferNumberEntity( | ||||
|                     device, | ||||
|                     "woofer_level", | ||||
|                     device.woofer_level, | ||||
|                     device.set_woofer, | ||||
|                     (-6, 12), | ||||
|                 ) | ||||
|             ) | ||||
|     async_add_entities(entities) | ||||
|     return True | ||||
| 
 | ||||
| 
 | ||||
| class SoundbarNumberEntity(NumberEntity): | ||||
| class SoundbarWooferNumberEntity(NumberEntity): | ||||
|     def __init__( | ||||
|         self, | ||||
|         device: SoundbarDevice, | ||||
|         append_unique_id: str, | ||||
|         state_function, | ||||
|         on_function, | ||||
|         min_max: tuple, | ||||
|     ): | ||||
|         self.entity_id = f"number.{device.device_name}_{append_unique_id}" | ||||
| 
 | ||||
|         self.entity_description = NumberEntityDescription(native_max_value=6, | ||||
|                                                           native_min_value=-10, | ||||
|                                                           mode=NumberMode.BOX, | ||||
|                                                           native_step=1, | ||||
|                                                           native_unit_of_measurement="dB", | ||||
|                                                           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,27 +51,19 @@ class SoundbarNumberEntity(NumberEntity): | |||
|             model=self.__device.model, | ||||
|             sw_version=self.__device.firmware_version, | ||||
|         ) | ||||
| 
 | ||||
|         self.__current_value_function = state_function | ||||
|         self.__set_value_function = on_function | ||||
|         self.__min_value = min_max[0] | ||||
|         self.__max_value = min_max[1] | ||||
|         self.__append_unique_id = append_unique_id | ||||
| 
 | ||||
|     # ---------- GENERAL --------------- | ||||
| 
 | ||||
|     @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 | ||||
|         return self.__device.woofer_level | ||||
| 
 | ||||
|     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) | ||||
|         await self.__device.set_woofer(int(value)) | ||||
|  |  | |||
|  | @ -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) | ||||
|  | @ -0,0 +1,4 @@ | |||
| dist | ||||
| node_modules | ||||
| .output | ||||
| .nuxt | ||||
|  | @ -0,0 +1,8 @@ | |||
| module.exports = { | ||||
|   root: true, | ||||
|   extends: '@nuxt/eslint-config', | ||||
|   rules: { | ||||
|     'vue/max-attributes-per-line': 'off', | ||||
|     'vue/multi-word-component-names': 'off' | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,12 @@ | |||
| node_modules | ||||
| *.iml | ||||
| .idea | ||||
| *.log* | ||||
| .nuxt | ||||
| .vscode | ||||
| .DS_Store | ||||
| coverage | ||||
| dist | ||||
| sw.* | ||||
| .env | ||||
| .output | ||||
|  | @ -0,0 +1,2 @@ | |||
| shamefully-hoist=true | ||||
| strict-peer-dependencies=false | ||||
|  | @ -0,0 +1,57 @@ | |||
| # Docus Starter | ||||
| 
 | ||||
| Starter template for [Docus](https://docus.dev). | ||||
| 
 | ||||
| ## Clone | ||||
| 
 | ||||
| Clone the repository (using `nuxi`): | ||||
| 
 | ||||
| ```bash | ||||
| npx nuxi init -t themes/docus | ||||
| ``` | ||||
| 
 | ||||
| ## Setup | ||||
| 
 | ||||
| Install dependencies: | ||||
| 
 | ||||
| ```bash | ||||
| yarn install | ||||
| ``` | ||||
| 
 | ||||
| ## Development | ||||
| 
 | ||||
| ```bash | ||||
| yarn dev | ||||
| ``` | ||||
| 
 | ||||
| ## Edge Side Rendering | ||||
| 
 | ||||
| Can be deployed to Vercel Functions, Netlify Functions, AWS, and most Node-compatible environments. | ||||
| 
 | ||||
| Look at all the available presets [here](https://v3.nuxtjs.org/guide/deploy/presets). | ||||
| 
 | ||||
| ```bash | ||||
| yarn build | ||||
| ``` | ||||
| 
 | ||||
| ## Static Generation | ||||
| 
 | ||||
| Use the `generate` command to build your application. | ||||
| 
 | ||||
| The HTML files will be generated in the .output/public directory and ready to be deployed to any static compatible hosting. | ||||
| 
 | ||||
| ```bash | ||||
| yarn generate | ||||
| ``` | ||||
| 
 | ||||
| ## Preview build | ||||
| 
 | ||||
| You might want to preview the result of your build locally, to do so, run the following command: | ||||
| 
 | ||||
| ```bash | ||||
| yarn preview | ||||
| ``` | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| For a detailed explanation of how things work, check out [Docus](https://docus.dev). | ||||
|  | @ -0,0 +1,37 @@ | |||
| export default defineAppConfig({ | ||||
|   docus: { | ||||
|     title: 'YASSI', | ||||
|     description: 'HomeAssistant: Yet another Samsung soundbar integration', | ||||
|     image: 'https://user-images.githubusercontent.com/904724/185365452-87b7ca7b-6030-4813-a2db-5e65c785bf88.png', | ||||
|     socials: { | ||||
|       github: 'samuelspagl/ha_samsung_soundbar', | ||||
|       nuxt: { | ||||
|         label: 'Nuxt', | ||||
|         icon: 'simple-icons:nuxtdotjs', | ||||
|         href: 'https://nuxt.com' | ||||
|       } | ||||
|     }, | ||||
|     github: { | ||||
|       dir: 'docs/content', | ||||
|       branch: 'main', | ||||
|       repo: 'ha_samsung_soundbar', | ||||
|       owner: 'samuelspagl', | ||||
|       edit: true | ||||
|     }, | ||||
|     aside: { | ||||
|       level: 0, | ||||
|       collapsed: false, | ||||
|       exclude: [] | ||||
|     }, | ||||
|     main: { | ||||
|       padded: true, | ||||
|       fluid: true | ||||
|     }, | ||||
|     header: { | ||||
|       logo: true, | ||||
|       showLinkIcon: true, | ||||
|       exclude: [], | ||||
|       fluid: true | ||||
|     } | ||||
|   } | ||||
| }) | ||||
|  | @ -0,0 +1,69 @@ | |||
| --- | ||||
| title: "YASSI" | ||||
| --- | ||||
| ::block-hero | ||||
| --- | ||||
| cta: | ||||
|   - Why another HomeAssistant integration? | ||||
|   - #why-another-integration | ||||
| secondary: | ||||
|   - Open on GitHub → | ||||
|   - https://github.com/nuxtlabs/docus | ||||
| snippet: | ||||
|   - Custom Components | ||||
|   - "- input selection" | ||||
|   - "- soundmode selection" | ||||
|   - "- eq-preset selection" | ||||
|   - "- woofer settings" | ||||
|   - "- other cool things" | ||||
| --- | ||||
| 
 | ||||
| #title | ||||
| Yassi | ||||
| 
 | ||||
| #description | ||||
| Yet another Samsung soundbar integration for HomeAssistant | ||||
| :: | ||||
| 
 | ||||
| ::card-grid | ||||
| #title | ||||
| Quick-Start | ||||
| #root | ||||
| :ellipsis | ||||
| 
 | ||||
| #default | ||||
|   ::card | ||||
|   #title | ||||
|   Getting Started. | ||||
|   #description | ||||
|   Go, Go, Go... Here you will find information on "How to install / configure". | ||||
|   <br> | ||||
|   <br> | ||||
|   :button-link[click here]{href="/getting-started"} | ||||
|   :: | ||||
| 
 | ||||
|   ::card | ||||
|   #title | ||||
|   Features | ||||
|   #description | ||||
|   Many cool features are awaiting your eyes to see ✨. | ||||
|   <br> | ||||
|   <br> | ||||
|   :button-link[click here]{href="/features"} | ||||
|   :: | ||||
| :: | ||||
| 
 | ||||
| 
 | ||||
| ## Why another integration | ||||
| 
 | ||||
| The current Samsung Soundbar Integration by @PiotrMachowski / @thierryBourbon are already pretty cool. | ||||
| But I wanted it to appear as a device, and base the Foundation on the `pysmartthings` python package. | ||||
| 
 | ||||
| Additionally, I wanted full control over the *Soundmode* and more. So I tried out a few things with the API, | ||||
| and found that also the **Subwoofer** as well as the **Equalizer** are controllable. | ||||
| 
 | ||||
| I created a new wrapper around the `pysmartthings.DeviceEntity` specifically set up for a Soundbar, and this | ||||
| is the Result. | ||||
| 
 | ||||
| I hope to integrate also controls for **surround speaker** as well as **Space-Fit Sound**, but as these features | ||||
| are not documented... ;)  | ||||
|  | @ -0,0 +1,30 @@ | |||
| # Getting Started | ||||
| 
 | ||||
| ## Installation | ||||
| 
 | ||||
| ### HACS (official) | ||||
| 
 | ||||
| > ⚠️ Not done yet, hopefully soon. | ||||
| 
 | ||||
| 
 | ||||
| ### HACS (custom repository) | ||||
| 
 | ||||
| You can add this repository as a custom repository to your hacs. | ||||
| After you've done that, you can search for it like with the "official" | ||||
| integrations. | ||||
| 
 | ||||
| ### Manual | ||||
| 
 | ||||
| Copy the contents of `custom_components/samsung_soundbar` to `config/custom_components/samsung_soundbar` | ||||
| on your HomeAssistant instance. | ||||
| 
 | ||||
| ## Configuration | ||||
| 
 | ||||
| After you installed the custom component, it should be possible to configure the integration | ||||
| in the `device` settings of your HomeAssistant. | ||||
| 
 | ||||
| You will need: | ||||
| - a SmartThings `api_key` [click here](https://account.smartthings.com/tokens) | ||||
| - the `device_id` of your device [click here](https://my.smartthings.com/advanced/devices) | ||||
| - a name for your Soundbar | ||||
| - and a `max_volume` | ||||
|  | @ -0,0 +1,35 @@ | |||
| # Features | ||||
| 
 | ||||
| **YASSI** and retrieve / set the status of the following features grouped as a device: | ||||
| - `media_player`: | ||||
|   - `on / off` [*read, write*] | ||||
|   - `volume` (set, step) [*read, write*] | ||||
|   - `input` (select) [*read*, write*] | ||||
|   - `sound_mode` (select) [*read, write*] | ||||
|   - `play` (button) [*write*] | ||||
|   - `pause` (button) [*write*] | ||||
|   - `media_artwork` (image) [*read*] | ||||
|   - `media_title` (text) [*read*] | ||||
|   - `media_artist` (text) [*read*] | ||||
| 
 | ||||
| - `number` | ||||
|   - **Woofer** | ||||
|     - level (set) [*read, write*] | ||||
| - `select` | ||||
|   - **Input** | ||||
|     - `input` [*read, write*] | ||||
|     - `supported_inputs` [*read*] | ||||
|   - **Soundmode** | ||||
|     - `active_soundmode` [*read, write*] | ||||
|     - `supported_soundmodes` [*read*] | ||||
|   - **EQ-Preset** | ||||
|     - `active_eq_preset` [*read, write*] | ||||
|     - `supported_eq_preset` [*read*] | ||||
| 
 | ||||
| - `button` | ||||
|   - `night_mode` [*read, write*] | ||||
|   - `voice_amplifier` [*read, write*] | ||||
|   - `bass_mode` [*read, write*] | ||||
| 
 | ||||
| - `image` | ||||
|   - `media_coverart` [*read*] | ||||
|  | @ -0,0 +1,47 @@ | |||
| # "Standard" information | ||||
| 
 | ||||
| This is the "standard" information that you can fetch with the `pysmartthings` library | ||||
| for a given soundbar: | ||||
| 
 | ||||
| ```python | ||||
| { | ||||
|     'supportedPlaybackCommands': status(value=['play', 'pause', 'stop'], unit=None, data=None), | ||||
|     'playbackStatus': status(value='paused', unit=None, data=None), | ||||
|     'mode': status(value=10, unit=None, data=None), | ||||
|     'detailName': status(value='TV', unit=None, data=None), | ||||
|     'volume': status(value=16, unit='%', data=None), | ||||
|     'supportedInputSources': status(value=['digital', 'HDMI1', 'bluetooth', 'wifi'], unit=None, data=None), | ||||
|     'inputSource': status(value='digital', unit=None, data=None), | ||||
|     'data': status(value=None,unit=None,data=None), | ||||
|     'switch': status(value='on', unit=None, data=None), | ||||
|     'role': status(value=None, unit=None, data=None), | ||||
|     'channel': status(value=None, unit=None, data=None), | ||||
|     'masterName': status(value=None, unit=None, data=None), | ||||
|     'status': status(value=None, unit=None, data=None), | ||||
|     'st': status(value='1970-01-01T00:00:28Z', unit=None, data=None), | ||||
|     'mndt': status(value='2022-01-01', unit=None, data=None), | ||||
|     'mnfv': status(value='HW-Q935BWWB-1010.0', unit=None, data=None), | ||||
|     'mnhw': status(value='', unit=None, data=None), | ||||
|     'di': status(value='##############################', unit=None, data=None), | ||||
|     'mnsl': status(value=None, unit=None, data=None), | ||||
|     'dmv': status(value='res.1.1.0,sh.1.1.0', unit=None, data=None), | ||||
|     'n': status(value='Samsung Soundbar Q935B', unit=None, data=None), | ||||
|     'mnmo': status(value='HW-Q935B', unit=None, data=None), | ||||
|     'vid': status(value='VD-NetworkAudio-002S', unit=None, data=None), | ||||
|     'mnmn': status(value='Samsung Electronics', unit=None, data=None), | ||||
|     'mnml': status(value=None, unit=None, data=None), | ||||
|     'mnpv': status(value='6.5', unit=None, data=None), | ||||
|     'mnos': status(value='Tizen', unit=None, data=None), | ||||
|     'pi': status(value='##################################', unit=None, data=None), | ||||
|     'icv': status(value='core.1.1.0', unit=None, data=None), | ||||
|     'mute': status(value='unmuted', unit=None, data=None), | ||||
|     'totalTime': status(value=174590, unit=None, data=None), | ||||
|     'audioTrackData': status(value={'title': 'QUIET', 'artist': 'ELEVATION RHYTHM', 'album': ''}, unit=None, data=None), | ||||
|     'elapsedTime': status(value=28601, unit=None, data=None) | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| It is possible to fetch the current status (on/off) or information about the input. | ||||
| and if Spotify / AirPlay or Bluetooth are used, also the `title` and `artist` of a played track. | ||||
| 
 | ||||
| All of these states can also be set. Eg. the input, volume, mute and more. | ||||
|  | @ -0,0 +1,234 @@ | |||
| # Additional information | ||||
| 
 | ||||
| It is possible to retrieve even more information / control more aspects of | ||||
| your Samsung soundbar, by utilizing the (undocumented) execute status. | ||||
| 
 | ||||
| As the [API states](https://developer.smartthings.com/docs/api/public/#operation/executeDeviceCommands),  | ||||
| it is possible to execute custom commands. You can retrieve the status / values of your | ||||
| custom command in the `data` attribute when fetching new information of the device. | ||||
| 
 | ||||
| <details> | ||||
| <summary>Expand to see a sample of the fetched data of a soundbar device</summary> | ||||
| 
 | ||||
| This is a dictionary fetched by a `pysmartthings.device.status.attributes` after a `device.status.refresh()`. | ||||
| 
 | ||||
| ```python | ||||
| { | ||||
|     'supportedPlaybackCommands': status(value=['play', 'pause', 'stop'], unit=None, data=None), | ||||
|     'playbackStatus': status(value='paused', unit=None, data=None), | ||||
|     'mode': status(value=10, unit=None, data=None), | ||||
|     'detailName': status(value='TV', unit=None, data=None), | ||||
|     'volume': status(value=16, unit='%', data=None), | ||||
|     'supportedInputSources': status(value=['digital', 'HDMI1', 'bluetooth', 'wifi'], unit=None, data=None), | ||||
|     'inputSource': status(value='digital', unit=None, data=None), | ||||
|     'data': status( | ||||
|         value={ | ||||
|             'payload': { | ||||
|                 'rt': ['x.com.samsung.networkaudio.eq'], | ||||
|                 'if': ['oic.if.rw', 'oic.if.baseline'], | ||||
|                 'x.com.samsung.networkaudio.supportedList': ['NONE', 'POP', 'JAZZ', 'CLASSIC', 'CUSTOM'], | ||||
|                 'x.com.samsung.networkaudio.EQname': 'NONE', | ||||
|                 'x.com.samsung.networkaudio.action': 'setEQmode', | ||||
|                 'x.com.samsung.networkaudio.EQband': ['0', '0', '0', '0', '0', '0', '0'] | ||||
|             } | ||||
|         }, | ||||
|         unit=None, | ||||
|         data={'href': '/sec/networkaudio/eq'} | ||||
|     ), | ||||
|     'switch': status(value='on', unit=None, data=None), | ||||
|     'role': status(value=None, unit=None, data=None), | ||||
|     'channel': status(value=None, unit=None, data=None), | ||||
|     'masterName': status(value=None, unit=None, data=None), | ||||
|     'status': status(value=None, unit=None, data=None), | ||||
|     'st': status(value='1970-01-01T00:00:28Z', unit=None, data=None), | ||||
|     'mndt': status(value='2022-01-01', unit=None, data=None), | ||||
|     'mnfv': status(value='HW-Q935BWWB-1010.0', unit=None, data=None), | ||||
|     'mnhw': status(value='', unit=None, data=None), | ||||
|     'di': status(value='##############################', unit=None, data=None), | ||||
|     'mnsl': status(value=None, unit=None, data=None), | ||||
|     'dmv': status(value='res.1.1.0,sh.1.1.0', unit=None, data=None), | ||||
|     'n': status(value='Samsung Soundbar Q935B', unit=None, data=None), | ||||
|     'mnmo': status(value='HW-Q935B', unit=None, data=None), | ||||
|     'vid': status(value='VD-NetworkAudio-002S', unit=None, data=None), | ||||
|     'mnmn': status(value='Samsung Electronics', unit=None, data=None), | ||||
|     'mnml': status(value=None, unit=None, data=None), | ||||
|     'mnpv': status(value='6.5', unit=None, data=None), | ||||
|     'mnos': status(value='Tizen', unit=None, data=None), | ||||
|     'pi': status(value='##################################', unit=None, data=None), | ||||
|     'icv': status(value='core.1.1.0', unit=None, data=None), | ||||
|     'mute': status(value='unmuted', unit=None, data=None), | ||||
|     'totalTime': status(value=174590, unit=None, data=None), | ||||
|     'audioTrackData': status(value={'title': 'QUIET', 'artist': 'ELEVATION RHYTHM', 'album': ''}, unit=None, data=None), | ||||
|     'elapsedTime': status(value=28601, unit=None, data=None) | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| </details> | ||||
| 
 | ||||
| The `data` attribute can also be fetched separately with an undocumented API endpoint. | ||||
| ```python | ||||
|     url = f"https://api.smartthings.com/v1/devices/{self._device_id}/components/main/capabilities/execute/status" | ||||
| ``` | ||||
| 
 | ||||
| It seems that the normal `device.status.refresh()` retrieves cached results from the execute status. Therefore | ||||
| using this endpoint separately seems to be a better solution. | ||||
| 
 | ||||
| To set the status of a given setting a command needs to be issued with the following (sample) structure: | ||||
| 
 | ||||
| ```python | ||||
| data = { | ||||
|             "commands": [ | ||||
|                 { | ||||
|                     "component": component_id, | ||||
|                     "capability": capability, | ||||
|                     "command": command, | ||||
|                     "arguments": ["/sec/networkaudio/advancedaudio"] | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
| ``` | ||||
| To set a setting, you will "update" an object in the given path, with a payload | ||||
| similar to the following: | ||||
| ```python | ||||
| data = { | ||||
|             "commands": [ | ||||
|                 { | ||||
|                     "component": component_id, | ||||
|                     "capability": capability, | ||||
|                     "command": command, | ||||
|                     "arguments": ["/sec/networkaudio/advancedaudio", {"x.com.samsung.networkaudio.bassboost": 1}] | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
| ``` | ||||
| 
 | ||||
| ## Soundmode | ||||
| 
 | ||||
| This setting has the href: `"/sec/networkaudio/soundmode"` | ||||
| 
 | ||||
| <details> | ||||
| <summary> | ||||
| A sample status looks like this: | ||||
| </summary> | ||||
| 
 | ||||
| ```python | ||||
| { | ||||
|     'data': { | ||||
|         'value': { | ||||
|             'payload': { | ||||
|                 'rt': ['x.com.samsung.networkaudio.soundmode'], | ||||
|                 'if': ['oic.if.a', 'oic.if.baseline'], | ||||
|                 'x.com.samsung.networkaudio.soundmode': 'adaptive sound', | ||||
|                 'x.com.samsung.networkaudio.supportedSoundmode': ['standard', 'surround', 'game', 'adaptive sound'] | ||||
|             } | ||||
|         }, | ||||
|         'data': {'href': '/sec/networkaudio/soundmode'}, | ||||
|         'timestamp': '2023-09-05T14:59:50.581Z' | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| </details> | ||||
| 
 | ||||
| ## Advanced Audio | ||||
| 
 | ||||
| This setting has the href: `"/sec/networkaudio/advancedaudio"` | ||||
| 
 | ||||
| <details> | ||||
| <summary> | ||||
| A sample status looks like this: | ||||
| </summary> | ||||
| 
 | ||||
| ```python | ||||
| { | ||||
|     'data': { | ||||
|         'value': { | ||||
|             'payload': { | ||||
|                 'rt': ['x.com.samsung.networkaudio.advancedaudio'], | ||||
|                 'if': ['oic.if.rw', 'oic.if.baseline'], | ||||
|                 'x.com.samsung.networkaudio.voiceamplifier': 0, | ||||
|                 'x.com.samsung.networkaudio.bassboost': 0, | ||||
|                 'x.com.samsung.networkaudio.nightmode': 0 | ||||
|             } | ||||
|         }, | ||||
|         'data': {'href': '/sec/networkaudio/advancedaudio'}, | ||||
|         'timestamp': '2023-09-05T15:00:14.665Z' | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| ``` | ||||
| </details> | ||||
| 
 | ||||
| ## Subwoofer | ||||
| 
 | ||||
| This setting has the href: `"/sec/networkaudio/woofer"` | ||||
| 
 | ||||
| <details> | ||||
| <summary> | ||||
| A sample status looks like this: | ||||
| </summary> | ||||
| 
 | ||||
| ```python | ||||
| { | ||||
|     'value': { | ||||
|         'payload': { | ||||
|             'rt': ['x.com.samsung.networkaudio.woofer'], | ||||
|             'if': ['oic.if.a', 'oic.if.baseline'], | ||||
|             'x.com.samsung.networkaudio.woofer': 3, | ||||
|             'x.com.samsung.networkaudio.connection': 'on' | ||||
|         } | ||||
|     }, | ||||
|     'data': {'href': '/sec/networkaudio/woofer'}, | ||||
|     'timestamp': '2023-09-05T14:57:36.450Z' | ||||
| } | ||||
| 
 | ||||
| ``` | ||||
| </details> | ||||
| 
 | ||||
| ## Equalizer | ||||
| 
 | ||||
| This setting has the href: `"/sec/networkaudio/eq"` | ||||
| 
 | ||||
| <details> | ||||
| <summary> | ||||
| A sample status looks like this: | ||||
| </summary> | ||||
| 
 | ||||
| ```python | ||||
| { | ||||
|     'data': { | ||||
|         'value': { | ||||
|             'payload': { | ||||
|                 'rt': ['x.com.samsung.networkaudio.eq'], | ||||
|                 'if': ['oic.if.rw', 'oic.if.baseline'], | ||||
|                 'x.com.samsung.networkaudio.supportedList': ['NONE', 'POP', 'JAZZ', 'CLASSIC', 'CUSTOM'], | ||||
|                 'x.com.samsung.networkaudio.EQname': 'NONE', | ||||
|                 'x.com.samsung.networkaudio.action': 'setEQmode', | ||||
|                 'x.com.samsung.networkaudio.EQband': ['0', '0', '0', '0', '0', '0', '0'] | ||||
|             } | ||||
|         }, | ||||
|         'data': {'href': '/sec/networkaudio/eq'}, | ||||
|         'timestamp': '2023-09-05T14:59:03.490Z' | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| </details> | ||||
| 
 | ||||
| ## Volume | ||||
| 
 | ||||
| This setting has the href: `"/sec/networkaudio/audio"` | ||||
| 
 | ||||
| <details> | ||||
| <summary> | ||||
| A sample status looks like this: | ||||
| </summary> | ||||
| 
 | ||||
| ```python | ||||
| { | ||||
|     'data': { | ||||
|         'value': {'payload': {'rt': ['oic.r.audio'], 'if': ['oic.if.a', 'oic.if.baseline'], 'mute': False, 'volume': 3}}, | ||||
|         'data': {'href': '/sec/networkaudio/audio'}, | ||||
|         'timestamp': '2023-09-05T15:09:04.980Z' | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| </details> | ||||
|  | @ -0,0 +1,13 @@ | |||
| export default defineNuxtConfig({ | ||||
|   // https://github.com/nuxt-themes/docus
 | ||||
|   extends: '@nuxt-themes/docus', | ||||
|   app: { | ||||
|     baseURL: '/ha_samsung_soundbar/', | ||||
|     }, | ||||
|   modules: [ | ||||
|     // https://github.com/nuxt-modules/plausible
 | ||||
|     '@nuxtjs/plausible', | ||||
|     // https://github.com/nuxt/devtools
 | ||||
|     '@nuxt/devtools' | ||||
|   ] | ||||
| }) | ||||
|  | @ -0,0 +1,21 @@ | |||
| { | ||||
|   "name": "YASSI", | ||||
|   "version": "0.1.0", | ||||
|   "private": true, | ||||
|   "scripts": { | ||||
|     "dev": "nuxi dev", | ||||
|     "build": "nuxi build", | ||||
|     "generate": "nuxi generate", | ||||
|     "preview": "nuxi preview", | ||||
|     "lint": "eslint ." | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@nuxt-themes/docus": "^1.13.1", | ||||
|     "@nuxt/devtools": "^0.6.7", | ||||
|     "@nuxt/eslint-config": "^0.1.1", | ||||
|     "@nuxtjs/plausible": "^0.2.1", | ||||
|     "@types/node": "^20.4.0", | ||||
|     "eslint": "^8.44.0", | ||||
|     "nuxt": "^3.6.2" | ||||
|   } | ||||
| } | ||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 15 KiB | 
|  | @ -0,0 +1,8 @@ | |||
| { | ||||
|     "extends": [ | ||||
|       "@nuxtjs" | ||||
|     ], | ||||
|     "lockFileMaintenance": { | ||||
|       "enabled": true | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,4 @@ | |||
| import { defineTheme } from 'pinceau' | ||||
| 
 | ||||
| export default defineTheme({ | ||||
| }) | ||||
|  | @ -0,0 +1,3 @@ | |||
| { | ||||
|   "extends": "./.nuxt/tsconfig.json" | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1,36 @@ | |||
| class Stuff: | ||||
|     def __init__(self, a: int): | ||||
|         self.__a = a | ||||
| 
 | ||||
|     def set_a(self, new_a: int): | ||||
|         self.__a = new_a | ||||
| 
 | ||||
|     @property | ||||
|     def a(self): | ||||
|         return self.__a | ||||
| 
 | ||||
| 
 | ||||
| class Stuff2: | ||||
|     def __init__(self, a: int): | ||||
|         self.stuff = Stuff(a) | ||||
| 
 | ||||
| 
 | ||||
|     def set_a(self, a): | ||||
|         self.stuff.set_a(a) | ||||
| 
 | ||||
|     @property | ||||
|     def a(self): | ||||
|         return self.stuff.a | ||||
| 
 | ||||
| hal = Stuff(3) | ||||
| print(hal.a) | ||||
| hal.set_a(5) | ||||
| print(hal.a) | ||||
| print() | ||||
| print() | ||||
| has = Stuff2(3) | ||||
| print(has.a) | ||||
| print(has.stuff.set_a(5)) | ||||
| print(has.a) | ||||
| print(has.set_a(10)) | ||||
| print(has.a) | ||||
		Loading…
	
		Reference in New Issue