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:
Samuel Spagl 2023-09-07 14:49:20 +02:00 committed by GitHub
parent b7ff6d1eb0
commit f93019dd68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 9844 additions and 29 deletions

0
CHANGELOG.md Normal file
View File

View File

@ -40,8 +40,9 @@ are not documented... ;)
- bass level - bass level
- *[to come] equalizer bands* - *[to come] equalizer bands*
- `select` entity - `select` entity
- *[to come] sound mode* (additional control in the "Device" tab) - sound mode (additional control in the "Device" tab)
- *[to come] equalizer preset* - input (additional control in the "Device" tab)
- equalizer preset
## How to install it: ## How to install it:

View File

@ -18,7 +18,7 @@ from .models import DeviceConfig, SoundbarConfig
_LOGGER = logging.getLogger(__name__) _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: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@ -1,9 +1,13 @@
import asyncio
import datetime
import json import json
import time import time
from urllib.parse import quote from urllib.parse import quote
import logging
from pysmartthings import DeviceEntity from pysmartthings import DeviceEntity
from ..const import DOMAIN
log = logging.getLogger(__name__)
class SoundbarDevice: class SoundbarDevice:
def __init__( def __init__(
@ -33,6 +37,7 @@ class SoundbarDevice:
self.__media_title = "" self.__media_title = ""
self.__media_artist = "" self.__media_artist = ""
self.__media_cover_url = "" self.__media_cover_url = ""
self.__media_cover_url_update_time: datetime.datetime | None = None
self.__old_media_title = "" self.__old_media_title = ""
self.__max_volume = max_volume self.__max_volume = max_volume
@ -55,13 +60,24 @@ class SoundbarDevice:
] ]
if self.__media_title != self.__old_media_title: if self.__media_title != self.__old_media_title:
self.__old_media_title = self.__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_cover_url = await self.get_song_title_artwork(
self.__media_artist, self.__media_title self.__media_artist, self.__media_title
) )
async def _update_soundmode(self): async def _update_soundmode(self):
await self.update_execution_data(["/sec/networkaudio/soundmode"]) await self.update_execution_data(["/sec/networkaudio/soundmode"])
await asyncio.sleep(0.1)
payload = await self.get_execute_status() 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[ self.__supported_soundmodes = payload[
"x.com.samsung.networkaudio.supportedSoundmode" "x.com.samsung.networkaudio.supportedSoundmode"
] ]
@ -69,13 +85,31 @@ class SoundbarDevice:
async def _update_woofer(self): async def _update_woofer(self):
await self.update_execution_data(["/sec/networkaudio/woofer"]) await self.update_execution_data(["/sec/networkaudio/woofer"])
await asyncio.sleep(0.1)
payload = await self.get_execute_status() 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_level = payload["x.com.samsung.networkaudio.woofer"]
self.__woofer_connection = payload["x.com.samsung.networkaudio.connection"] self.__woofer_connection = payload["x.com.samsung.networkaudio.connection"]
async def _update_equalizer(self): async def _update_equalizer(self):
await self.update_execution_data(["/sec/networkaudio/eq"]) await self.update_execution_data(["/sec/networkaudio/eq"])
await asyncio.sleep(0.1)
payload = await self.get_execute_status() 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.__active_eq_preset = payload["x.com.samsung.networkaudio.EQname"]
self.__supported_eq_presets = payload[ self.__supported_eq_presets = payload[
"x.com.samsung.networkaudio.supportedList" "x.com.samsung.networkaudio.supportedList"
@ -85,7 +119,18 @@ class SoundbarDevice:
async def _update_advanced_audio(self): async def _update_advanced_audio(self):
await self.update_execution_data(["/sec/networkaudio/advancedaudio"]) await self.update_execution_data(["/sec/networkaudio/advancedaudio"])
await asyncio.sleep(0.1)
payload = await self.get_execute_status() 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.__night_mode = payload["x.com.samsung.networkaudio.nightmode"]
self.__bass_mode = payload["x.com.samsung.networkaudio.bassboost"] self.__bass_mode = payload["x.com.samsung.networkaudio.bassboost"]
self.__voice_amplifier = payload["x.com.samsung.networkaudio.voiceamplifier"] self.__voice_amplifier = payload["x.com.samsung.networkaudio.voiceamplifier"]
@ -132,7 +177,10 @@ class SoundbarDevice:
@property @property
def volume_level(self) -> float: 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 @property
def volume_muted(self) -> bool: def volume_muted(self) -> bool:
@ -144,7 +192,7 @@ class SoundbarDevice:
This respects the max volume and hovers between This respects the max volume and hovers between
:param volume: between 0 and 1 :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): async def mute_volume(self, mute: bool):
if mute: if mute:
@ -174,6 +222,7 @@ class SoundbarDevice:
property="x.com.samsung.networkaudio.woofer", property="x.com.samsung.networkaudio.woofer",
value=level, value=level,
) )
self.__woofer_level = level
# ------------ INPUT SOURCE ------------- # ------------ INPUT SOURCE -------------
@ -304,6 +353,10 @@ class SoundbarDevice:
return detail_status.value return detail_status.value
return None return None
@property
def media_coverart_updated(self) -> datetime.datetime:
return self.__media_cover_url_update_time
# ------------ SUPPORT FUNCTIONS ------------ # ------------ SUPPORT FUNCTIONS ------------
async def update_execution_data(self, argument: str): async def update_execution_data(self, argument: str):

View File

@ -1,8 +1,10 @@
import logging import logging
from datetime import datetime
from homeassistant.components.image import ImageEntity from homeassistant.components.image import ImageEntity
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.typing import UndefinedType
from .models import DeviceConfig from .models import DeviceConfig
from .api_extension.SoundbarDevice import SoundbarDevice from .api_extension.SoundbarDevice import SoundbarDevice
@ -41,9 +43,22 @@ class SoundbarImageEntity(ImageEntity):
sw_version=self.__device.firmware_version, sw_version=self.__device.firmware_version,
) )
self._attr_image_url = self.__device.media_coverart_url self.__updated = None
# ---------- GENERAL --------------- # ---------- 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 @property
def name(self): def name(self):

View File

@ -1,4 +1,5 @@
import logging import logging
from typing import Mapping, Any
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
DEVICE_CLASS_SPEAKER, DEVICE_CLASS_SPEAKER,
@ -189,3 +190,7 @@ class SmartThingsSoundbarMediaPlayer(MediaPlayerEntity):
async def async_media_stop(self): async def async_media_stop(self):
await self.device.media_stop() await self.device.media_stop()
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
return self.device.retrieve_data

View File

@ -1,6 +1,6 @@
import logging import logging
from homeassistant.components.number import NumberEntity from homeassistant.components.number import NumberEntity, NumberEntityDescription, NumberMode
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from .models import DeviceConfig from .models import DeviceConfig
@ -19,29 +19,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
device = device_config.device device = device_config.device
if device.device_id == config_entry.data.get(CONF_ENTRY_DEVICE_ID): if device.device_id == config_entry.data.get(CONF_ENTRY_DEVICE_ID):
entities.append( entities.append(
SoundbarNumberEntity( SoundbarWooferNumberEntity(
device, device,
"woofer_level", "woofer_level",
device.woofer_level,
device.set_woofer,
(-6, 12),
) )
) )
async_add_entities(entities) async_add_entities(entities)
return True return True
class SoundbarNumberEntity(NumberEntity): class SoundbarWooferNumberEntity(NumberEntity):
def __init__( def __init__(
self, self,
device: SoundbarDevice, device: SoundbarDevice,
append_unique_id: str, append_unique_id: str,
state_function,
on_function,
min_max: tuple,
): ):
self.entity_id = f"number.{device.device_name}_{append_unique_id}" 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.__device = device
self._attr_unique_id = f"{device.device_id}_sw_{append_unique_id}" self._attr_unique_id = f"{device.device_id}_sw_{append_unique_id}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
@ -51,27 +51,19 @@ class SoundbarNumberEntity(NumberEntity):
model=self.__device.model, model=self.__device.model,
sw_version=self.__device.firmware_version, 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
self.__min_value = min_max[0]
self.__max_value = min_max[1]
# ---------- GENERAL --------------- # ---------- GENERAL ---------------
@property @property
def name(self): def name(self):
return self.__device.device_name return self.__append_unique_id
# ------ STATE FUNCTIONS -------- # ------ STATE FUNCTIONS --------
@property @property
def native_value(self) -> float | None: 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): async def async_set_native_value(self, value: float):
if value > self.__max_value: await self.__device.set_woofer(int(value))
value = self.__min_value
if value < self.__min_value:
value = self.__min_value
await self.__set_value_function(value)

View File

@ -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)

4
docs/.eslintignore Normal file
View File

@ -0,0 +1,4 @@
dist
node_modules
.output
.nuxt

8
docs/.eslintrc.cjs Normal file
View File

@ -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'
}
}

12
docs/.gitignore vendored Executable file
View File

@ -0,0 +1,12 @@
node_modules
*.iml
.idea
*.log*
.nuxt
.vscode
.DS_Store
coverage
dist
sw.*
.env
.output

2
docs/.npmrc Normal file
View File

@ -0,0 +1,2 @@
shamefully-hoist=true
strict-peer-dependencies=false

57
docs/README.md Executable file
View File

@ -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).

37
docs/app.config.ts Normal file
View File

@ -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
}
}
})

69
docs/content/0.index.md Normal file
View File

@ -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... ;)

View File

@ -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`

View File

@ -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*]

View File

@ -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.

View File

@ -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>

13
docs/nuxt.config.ts Executable file
View File

@ -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'
]
})

21
docs/package.json Executable file
View File

@ -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"
}
}

BIN
docs/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

8
docs/renovate.json Executable file
View File

@ -0,0 +1,8 @@
{
"extends": [
"@nuxtjs"
],
"lockFileMaintenance": {
"enabled": true
}
}

4
docs/tokens.config.ts Normal file
View File

@ -0,0 +1,4 @@
import { defineTheme } from 'pinceau'
export default defineTheme({
})

3
docs/tsconfig.json Executable file
View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

8966
docs/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

36
test.py Normal file
View File

@ -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)