Compare commits

...

17 Commits

Author SHA1 Message Date
orkun c113688986 Update custom_components/samsung_soundbar/manifest.json 2025-04-10 19:37:48 +03:00
orkun 8cd2aa3d51 Update custom_components/samsung_soundbar/manifest.json 2025-04-10 19:36:41 +03:00
Samuel Spagl bd313ea27a
Fix 'audiotrackdata' (#42)
## [0.4.1] Media Mystique: The Great Data Disappearing Act!

### Fixed

- Made media data (*track title*, *artist*, *length*) optional to acoomodate soundbars that don't provide this information (🥲)

### Added

- Add translations for the english translation file
2024-10-14 21:46:14 +02:00
Samuel Spagl 0d2424b578
Feature: Add more fine grained configuration steps (#28)
> ⚠️ Please read the following carefully:
> This release is a bit special. As "something" on Samsung's side changed,
> it is currently not possible to retrieve the status of "custom capabilities", eg.
> woofer, soundmode, eq, and others. Therefore I decided to give the option to
> disable the entities of these features as the value of these entities is not trustworthy.
> Instead I implemented all of these and more (thanks to @whitebearded) as service calls.
> Have fun using them!

### Added

- Configuration flow options for enable / disable
  - "advanced audio" features (NightMode, Bassmode, VoiceEnhancer)
  - "woofer" feature
  - "soundmode" feature
  - "eq" feature
- added `media_player` support for next and previous track
- Service calls for:
  - "advanced audio" features (NightMode, Bassmode, VoiceEnhancer)
  - "woofer" feature
  - "soundmode" feature
  - "speaker_level"
  - "rear_speaker_mode"
  - "space_fit_sound"
  - "active_voice_amplifier"

### Changed

- Fixed state, also displaying "playing" and "paused" values

---------

Co-authored-by: Samuel Spagl <samuel.spagl@kobil.com>
2024-06-09 17:13:38 +02:00
Samuel Spagl 9bc8be7861
Add MIT LICENSE 2024-04-08 21:53:29 +02:00
Samuel Spagl 14e30ba970
Update README.md
Add note about current API issues
2024-04-05 16:58:44 +02:00
Samuel Spagl 430f6a1840
Add additional validation for CONF_ENTRY_MAX_VOLUME value in the setup process to be greater than zero (#24)
Fixes #19
2024-04-01 10:49:48 +02:00
Samuel Spagl 3bcabb8c77
Enhance documentation (#25)
---------

Co-authored-by: Jorim Tielemans <tielemans.jorim@gmail.com>
2024-03-15 15:10:11 +01:00
Samuel Spagl 5e24680d5d
Add icons to select, sensor and switches, change some logging statements (#16)
#### Added

- Icons for the individual entities

#### Changed

- Updated the GitHub actions workflows
- Change "magic numbers" to MediaPlayerEntityFeature object. For more information see https://developers.home-assistant.io/blog/2023/12/28/support-feature-magic-numbers-deprecation
- `source` now returns the value `wifi` when the `media_app_name` is *AirPlay* or *Spotify*
- removed some unnecessary logging statements, and changed others to debug
2024-03-12 14:39:12 +01:00
Samuel Spagl dd61dec79f
🎨 Chore: format files with `black` and `isort`
The title says it all, still for more information take a look at the merge-request #15.
---------

Co-authored-by: samuelspagl <samuel@spagl-media.de>
2024-02-08 16:58:57 +01:00
Samuel Spagl 30ef090d9e
🐛: add volume `sensor` entity, fix sound_mode/source in `media_player` entity
This merge-request adds new functionality as well as fixes a minor bug.
For more information see #9.

- New sensor entity added
- Disabled extra_state_attributes to correctly display available sources/soundmodes in the media_player configuration.
---------

Co-authored-by: samuelspagl <samuel@spagl-media.de>
2024-02-08 16:45:32 +01:00
samuelspagl 78e825157e delete test.py 2023-09-07 15:56:13 +02:00
Samuel Spagl 79261aa4d1
fix ko-fi link 2023-09-07 15:35:02 +02:00
Samuel Spagl 0c9a317a7e
Add ko-fi link 2023-09-07 15:33:03 +02:00
Samuel Spagl bfe269f608
Update nuxt.config.ts 2023-09-07 14:53:45 +02:00
Samuel Spagl f93019dd68
various improvements (#5)
- Added various improvements and new features.
- First version of a documentation
---------

Co-authored-by: samuelspagl <samuel@spagl-media.de>
2023-09-07 14:49:20 +02:00
Samuel Spagl b7ff6d1eb0
Update CI-Job 2023-09-06 19:10:38 +02:00
47 changed files with 12441 additions and 1076 deletions

View File

@ -0,0 +1,32 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python
{
"name": "Python 3",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "homeassistant/home-assistant:dev",
"postCreateCommand": "scripts/setup",
"forwardPorts": [
8123
],
"portsAttributes": {
"8123": {
"label": "Home Assistant",
"onAutoForward": "notify"
}
}
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "pip3 install --user -r requirements.txt",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
ko_fi: samuelspagl

View File

@ -15,7 +15,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: "Checkout the repository" - name: "Checkout the repository"
uses: "actions/checkout@v3.5.3" uses: "actions/checkout@v4.1.0"
- name: "Adjust version number" - name: "Adjust version number"
shell: "bash" shell: "bash"
@ -32,4 +32,4 @@ jobs:
- name: "Upload the ZIP file to the release" - name: "Upload the ZIP file to the release"
uses: softprops/action-gh-release@v0.1.15 uses: softprops/action-gh-release@v0.1.15
with: with:
files: ${{ github.workspace }}/custom_components/samsung_soundbar/samsung_soundbar.zip files: ${{ github.workspace }}/custom_components/samsung_soundbar/samsung_soundbar.zip

6
.gitignore vendored
View File

@ -2,6 +2,12 @@
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
.DS_store
config
.vscode
.ruff_cache
# C extensions # C extensions
*.so *.so

93
CHANGELOG.md Normal file
View File

@ -0,0 +1,93 @@
# Changelog
## [0.4.1] Media Mystique: The Great Data Disappearing Act!
### Fixed
- Made media data (*track title*, *artist*, *length*) optional to acoomodate soundbars that don't provide this information (🥲)
### Added
- Add translations for the english translation file
## [0.4.0] Started with an "ick", but is now packed with new features 💪
> ⚠️ Please read the following carefully:
> This release is a bit special. As "something" on Samsung's side changed,
> it is currently not possible to retrieve the status of "custom capabilities", eg.
> woofer, soundmode, eq, and others. Therefore I decided to give the option to
> disable the entities of these features as the value of these entities is not trustworthy.
> Instead I implemented all of these and more (thanks to @whitebearded) as service calls.
> Have fun using them!
### Added
- Configuration flow options for enable / disable
- "advanced audio" features (NightMode, Bassmode, VoiceEnhancer)
- "woofer" feature
- "soundmode" feature
- "eq" feature
- added `media_player` support for next and previous track
- Service calls for:
- "advanced audio" features (NightMode, Bassmode, VoiceEnhancer)
- "woofer" feature
- "soundmode" feature
- "speaker_level"
- "rear_speaker_mode"
- "space_fit_sound"
- "active_voice_amplifier"
### Changed
- Fixed state, also displaying "playing" and "paused" values
## [0.3.2] Fix division by zero
### Added
- The config flow now also checks whether the `int` provided for `CONF_ENTRY_MAX_VOLUME` is
greater than `1` and lower than `100`. This will make sure that a division by zero cannot happen.
- Add default value `100` to `CONF_ENTRY_MAX_VOLUME`
## [0.3.1] Documentation enhancements
### Changed
- Updated the `README` as well as the documentation website
## [0.3.0] Icons and Chore
### Added
- Icons for the individual entities
### Changed
- Updated the GitHub actions workflows
- Change "magic numbers" to `MediaPlayerEntityFeature` object
For more information see https://developers.home-assistant.io/blog/2023/12/28/support-feature-magic-numbers-deprecation
- the `source` now returns the value `wifi` when the `media_app_name` is `AirPlay` or `Spotify`
- removed some unnecessary logging statements, and changed others to `debug`
## [0.2.1] Chore: Format repository - 2024-02-08
### Changed
- formatted the repository with black and isort
## [0.2.0] Add volume sensor - 2024-02-08
### Added
- add new sensor entity for the volume
### Fix
- remove `extra_state_attributes` from `media_player` instance:
The property caused some unwanted side-effects on some systems.
## [0.1.0] 🎉 First Version
### Added
- first version, gonna extend this Changelog sometime :D

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Samuel Spagl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -9,8 +9,7 @@ rich = "*"
homeassistant = "*" homeassistant = "*"
[dev-packages] [dev-packages]
black = "*" ruff = "*"
isort = "*"
[requires] [requires]
python_version = "3.11" python_version = "3.12"

2257
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,22 @@
# HomeAssistant: Samsung Soundbar Integration # YASSI: Yet Another Samsung Soundbar Integration (for Home Assistant)
> Yet another Samsung Soundbar Integration (YASSI) Welcome to YASSI, the Home Assistant integration designed to bring comprehensive control over your Samsung Soundbar into your smart home ecosystem.
> [!NOTE]
> Please use service calls for setting the attribute of a custom capability instead of the entity. (See #43 for more information)
**Table of Contents:**
<!-- TOC -->
* [Why YASSI](#why-yassi)
* [Features](#features)
* [Installation / Setup](#installation--setup)
* [Prerequisites](#prerequisites)
* [Installation:](#installation)
* [Configuration](#configuration)
* [Support](#support)
* [Contributing](#contributing)
* [General Thanks](#general-thanks)
<!-- TOC -->
## Why YASSI ## Why YASSI
@ -18,47 +34,54 @@ are not documented... ;)
## Features ## Features
- Set-Up through HomeAssistant-UI
- Theoretically it should be possible to have multiple Devices (not tested)
- `media_player` Entity - **UI Setup**: You can easily set up your Soundbar through the UI.
- On / Off - **Media Player Controls**: Power, volume, mute, source selection, and media controls are all at your fingertips.
- Volume - **Selectable Sound Modes**: Choose from various sound modes and inputs for optimal audio.
- Mute - **Subwoofer & Equalizer Adjustment**: Fine-tune your audio experience.
- Input Source - **Switchable Enhancements**: Toggle features like night mode and voice amplification.
- Sound Mode - **Customizable Bass Level**: Set the bass to your preference.
- Media - **Multiple Devices**: should be theoretically possible but **not** tested
- Play / Pause / Stop
- Artist
- Title
- Music Cover Art url (iTunes Api)
- `switch` entity
- Night mode
- Bass mode
- Voice amplifier
- `number` entity
- bass level
- *[to come] equalizer bands*
- `select` entity
- *[to come] sound mode* (additional control in the "Device" tab)
- *[to come] equalizer preset*
## How to install it: For the full feature list per entity type, please take a look at the [documentation](ha-samsung-soundbar.vercel.app) website.
### HACS: ## Installation / Setup
> ⚠️ not done yet but planned (hopefully)
### Adding this repository as custom repository ### Prerequisites
Add this repository as custom repository in HACS and install it ;) Before you begin, ensure you have the following:
### Manual - A Samsung Soundbar compatible with SmartThings.
- Home Assistant installed and running.
- HACS (Home Assistant Community Store) for easy installation.
You can also copy the `samsung_soundbar` folder in the `custom_components` folder to ### Installation
your `config/custom_components` folder.
1. Add this repository as a custom repository in HACS or manually copy the `samsung_soundbar` folder to the `custom_components` directory in your Home Assistant configuration.
[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=samuelspagl&repository=ha_samsung_soundbar&category=integration)
2. Restart Home Assistant.
> [!NOTE]
> It is planned to add it to the default `HACS` repository list, but not done yet.
### Configuration
To integrate your Samsung Soundbar with Home Assistant using YASSI, you will be asked for the following variables:
- **SmartThings API Key**: [Retrieve your API key from SmartThings Tokens.](https://account.smartthings.com/tokens)
- **Device ID**: [Find your device ID at SmartThings Devices.](https://my.smartthings.com/advanced/devices)
- **Device Name**: Choose a name for your soundbar to be recognized in Home Assistant.
- **Max Volume**: Define the maximum volume level for the `media_player` slider (between `1` and `100`).
## Support
For support, feature requests, or bug reporting, please visit the Issues section of this GitHub repository.
## Contributing
Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
## General Thanks ## General Thanks
Like already mentioned, thanks to @PiotrMachowski / @thierryBourbon for the general - Like already mentioned, thanks to @PiotrMachowski / @thierryBourbon for the general idea on how to do things.
idea on how to do things.

View File

@ -11,23 +11,30 @@ from .const import (
CONF_ENTRY_DEVICE_ID, CONF_ENTRY_DEVICE_ID,
CONF_ENTRY_DEVICE_NAME, CONF_ENTRY_DEVICE_NAME,
CONF_ENTRY_MAX_VOLUME, CONF_ENTRY_MAX_VOLUME,
SUPPORTED_DOMAINS, CONF_ENTRY_SETTINGS_ADVANCED_AUDIO_SWITCHES,
CONF_ENTRY_SETTINGS_EQ_SELECTOR,
CONF_ENTRY_SETTINGS_SOUNDMODE_SELECTOR,
CONF_ENTRY_SETTINGS_WOOFER_NUMBER,
DOMAIN, DOMAIN,
SUPPORTED_DOMAINS,
) )
from .models import DeviceConfig, SoundbarConfig 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", "sensor"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up component from a config entry, config_entry contains data from config entry database.""" """Set up component from a config entry, config_entry contains data from config entry database."""
# store shell object # store shell object
_LOGGER.info(f"[{DOMAIN}] Starting to setup ConfigEntry {entry.data}") _LOGGER.info(f"[{DOMAIN}] Starting to setup a ConfigEntry")
_LOGGER.debug(
f"[{DOMAIN}] Setting up ConfigEntry with the following data: {entry.data}"
)
if not DOMAIN in hass.data: if not DOMAIN in hass.data:
_LOGGER.info(f"[{DOMAIN}] Domain not found in hass.data setting default") _LOGGER.debug(f"[{DOMAIN}] Domain not found in hass.data setting default")
hass.data[DOMAIN] = SoundbarConfig( hass.data[DOMAIN] = SoundbarConfig(
SmartThings( SmartThings(
async_get_clientsession(hass), entry.data.get(CONF_ENTRY_API_KEY) async_get_clientsession(hass), entry.data.get(CONF_ENTRY_API_KEY)
@ -36,10 +43,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) )
domain_config: SoundbarConfig = hass.data[DOMAIN] domain_config: SoundbarConfig = hass.data[DOMAIN]
_LOGGER.info(f"[{DOMAIN}] Retrieved Domain Config: {domain_config}") _LOGGER.debug(f"[{DOMAIN}] Retrieved Domain Config: {domain_config}")
if not entry.data.get(CONF_ENTRY_DEVICE_ID) in domain_config.devices: if not entry.data.get(CONF_ENTRY_DEVICE_ID) in domain_config.devices:
_LOGGER.info( _LOGGER.info(f"[{DOMAIN}] Setting up new Soundbar device")
_LOGGER.debug(
f"[{DOMAIN}] DeviceId: {entry.data.get(CONF_ENTRY_DEVICE_ID)} not found in domain_config, setting up new device." f"[{DOMAIN}] DeviceId: {entry.data.get(CONF_ENTRY_DEVICE_ID)} not found in domain_config, setting up new device."
) )
smart_things_device = await domain_config.api.device( smart_things_device = await domain_config.api.device(
@ -47,17 +55,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) )
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
soundbar_device = SoundbarDevice( soundbar_device = SoundbarDevice(
smart_things_device, smart_things_device,
session, session,
entry.data.get(CONF_ENTRY_MAX_VOLUME), entry.data.get(CONF_ENTRY_MAX_VOLUME),
entry.data.get(CONF_ENTRY_DEVICE_NAME), entry.data.get(CONF_ENTRY_DEVICE_NAME),
) enable_eq=entry.data.get(CONF_ENTRY_SETTINGS_EQ_SELECTOR),
enable_advanced_audio=entry.data.get(
CONF_ENTRY_SETTINGS_ADVANCED_AUDIO_SWITCHES
),
enable_soundmode=entry.data.get(CONF_ENTRY_SETTINGS_SOUNDMODE_SELECTOR),
enable_woofer=entry.data.get(CONF_ENTRY_SETTINGS_WOOFER_NUMBER),
)
await soundbar_device.update() await soundbar_device.update()
domain_config.devices[entry.data.get(CONF_ENTRY_DEVICE_ID)] = DeviceConfig( domain_config.devices[entry.data.get(CONF_ENTRY_DEVICE_ID)] = DeviceConfig(
entry.data, entry.data, soundbar_device
soundbar_device
) )
_LOGGER.info(f"[{DOMAIN}] after initializing Soundbar device") _LOGGER.info(f"[{DOMAIN}] Successfully initialized new Soundbar device")
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True

View File

@ -1,13 +1,28 @@
import asyncio
import datetime
import json import json
import time import logging
from urllib.parse import quote from urllib.parse import quote
from pysmartthings import DeviceEntity from pysmartthings import DeviceEntity
from .const import SpeakerIdentifier, RearSpeakerMode
from ..const import DOMAIN
log = logging.getLogger(__name__)
class SoundbarDevice: class SoundbarDevice:
def __init__( def __init__(
self, device: DeviceEntity, session, max_volume: int, device_name: str self,
device: DeviceEntity,
session,
max_volume: int,
device_name: str,
enable_eq: bool = False,
enable_soundmode: bool = False,
enable_advanced_audio: bool = False,
enable_woofer: bool = False,
): ):
self.device = device self.device = device
self._device_id = self.device.device_id self._device_id = self.device.device_id
@ -15,17 +30,21 @@ class SoundbarDevice:
self.__session = session self.__session = session
self.__device_name = device_name self.__device_name = device_name
self.__enable_soundmode = enable_soundmode
self.__supported_soundmodes = [] self.__supported_soundmodes = []
self.__active_soundmode = "" self.__active_soundmode = ""
self.__enable_woofer = enable_woofer
self.__woofer_level = 0 self.__woofer_level = 0
self.__woofer_connection = "" self.__woofer_connection = ""
self.__enable_eq = enable_eq
self.__active_eq_preset = "" self.__active_eq_preset = ""
self.__supported_eq_presets = [] self.__supported_eq_presets = []
self.__eq_action = "" self.__eq_action = ""
self.__eq_bands = [] self.__eq_bands = []
self.__enable_advanced_audio = enable_advanced_audio
self.__voice_amplifier = 0 self.__voice_amplifier = 0
self.__night_mode = 0 self.__night_mode = 0
self.__bass_mode = 0 self.__bass_mode = 0
@ -33,6 +52,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
@ -41,27 +61,49 @@ class SoundbarDevice:
await self.device.status.refresh() await self.device.status.refresh()
await self._update_media() await self._update_media()
await self._update_soundmode()
await self._update_advanced_audio() if self.__enable_soundmode:
await self._update_woofer() await self._update_soundmode()
await self._update_equalizer() if self.__enable_advanced_audio:
await self._update_advanced_audio()
if self.__enable_soundmode:
await self._update_woofer()
if self.__enable_eq:
await self._update_equalizer()
async def _update_media(self): async def _update_media(self):
self.__media_artist = self.device.status._attributes["audioTrackData"].value[ if "audioTrackData" in self.device.status._attributes:
"artist" self.__media_artist = self.device.status._attributes["audioTrackData"].value[
] "artist"
self.__media_title = self.device.status._attributes["audioTrackData"].value[ ]
"title" self.__media_title = self.device.status._attributes["audioTrackData"].value[
] "title"
if self.__media_title != self.__old_media_title: ]
self.__old_media_title = self.__media_title if self.__media_title != self.__old_media_title:
self.__media_cover_url = await self.get_song_title_artwork( self.__old_media_title = self.__media_title
self.__media_artist, 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): async def _update_soundmode(self):
await self.update_execution_data(["/sec/networkaudio/soundmode"]) await self.update_execution_data(["/sec/networkaudio/soundmode"])
await asyncio.sleep(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(1)
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 +111,35 @@ 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 +149,20 @@ 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"]
@ -120,7 +197,15 @@ class SoundbarDevice:
@property @property
def state(self) -> str: def state(self) -> str:
return "on" if self.device.status.switch else "off" if self.device.status.switch:
if self.device.status.playback_status == "playing":
return "playing"
if self.device.status.playback_status == "paused":
return "paused"
else:
return "on"
else:
return "off"
async def switch_off(self): async def switch_off(self):
await self.device.switch_off(True) await self.device.switch_off(True)
@ -132,7 +217,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 +232,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,11 +262,14 @@ 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 -------------
@property @property
def input_source(self): def input_source(self):
if self.media_app_name in ("AirPlay", "Spotify"):
return "wifi"
return self.device.status.input_source return self.device.status.input_source
@property @property
@ -282,11 +373,15 @@ class SoundbarDevice:
@property @property
def media_duration(self) -> int | None: def media_duration(self) -> int | None:
return self.device.status.attributes.get("totalTime").value attr = self.device.status.attributes.get("totalTime", None)
if attr:
return attr.value
@property @property
def media_position(self) -> int | None: def media_position(self) -> int | None:
return self.device.status.attributes.get("elapsedTime").value attr = self.device.status.attributes.get("elapsedTime", None)
if attr:
return attr.value
async def media_play(self): async def media_play(self):
await self.device.play(True) await self.device.play(True)
@ -297,6 +392,12 @@ class SoundbarDevice:
async def media_stop(self): async def media_stop(self):
await self.device.stop(True) await self.device.stop(True)
async def media_next_track(self):
await self.device.command("main", "mediaPlayback", "fastForward")
async def media_previous_track(self):
await self.device.command("main", "mediaPlayback", "rewind")
@property @property
def media_app_name(self): def media_app_name(self):
detail_status = self.device.status.attributes.get("detailName", None) detail_status = self.device.status.attributes.get("detailName", None)
@ -304,21 +405,58 @@ 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
# ------------ Speaker Level ----------------
async def set_speaker_level(self, speaker: SpeakerIdentifier, level: int):
await self.set_custom_execution_data(
href="/sec/networkaudio/channelVolume",
property="x.com.samsung.networkaudio.channelVolume",
value=[{"name": speaker.value, "value": level}],
)
async def set_rear_speaker_mode(self, mode: RearSpeakerMode):
await self.set_custom_execution_data(
href="/sec/networkaudio/surroundspeaker",
property="x.com.samsung.networkaudio.currentRearPosition",
value=mode.value,
)
# ------------ OTHER FUNCTIONS ------------
async def set_active_voice_amplifier(self, enabled: bool):
await self.set_custom_execution_data(
href="/sec/networkaudio/activeVoiceAmplifier",
property="x.com.samsung.networkaudio.activeVoiceAmplifier",
value=1 if enabled else 0
)
async def set_space_fit_sound(self, enabled: bool):
await self.set_custom_execution_data(
href="/sec/networkaudio/spacefitSound",
property="x.com.samsung.networkaudio.spacefitSound",
value=1 if enabled else 0
)
# ------------ SUPPORT FUNCTIONS ------------ # ------------ SUPPORT FUNCTIONS ------------
async def update_execution_data(self, argument: str): async def update_execution_data(self, argument: str):
return await self.device.command("main", "execute", "execute", argument) stuff = await self.device.command("main", "execute", "execute", argument)
return stuff
async def set_custom_execution_data(self, href: str, property: str, value): async def set_custom_execution_data(self, href: str, property: str, value):
argument = [href, {property: value}] argument = [href, {property: value}]
await self.device.command("main", "execute", "execute", argument) assert await self.device.command("main", "execute", "execute", argument)
async def get_execute_status(self): async def get_execute_status(self):
url = f"https://api.smartthings.com/v1/devices/{self._device_id}/components/main/capabilities/execute/status" url = f"https://api.smartthings.com/v1/devices/{self._device_id}/components/main/capabilities/execute/status"
request_headers = {"Authorization": "Bearer " + self._api_key} request_headers = {"Authorization": "Bearer " + self._api_key}
resp = await self.__session.get(url, headers=request_headers) resp = await self.__session.get(url, headers=request_headers)
dict = await resp.json() dict_stuff = await resp.json()
return dict["data"]["value"]["payload"] return dict_stuff["data"]["value"]["payload"]
async def get_song_title_artwork(self, artist: str, title: str) -> str: async def get_song_title_artwork(self, artist: str, title: str) -> str:
""" """

View File

@ -0,0 +1,15 @@
from enum import Enum
class SpeakerIdentifier(Enum):
CENTER = "Spk_Center"
SIDE = "Spk_Side"
WIDE = "Spk_Wide"
FRONT_TOP = "Spk_Front_Top"
REAR = "Spk_Rear"
REAR_TOP = "Spk_Rear_Top"
class RearSpeakerMode(Enum):
FRONT = "Front"
REAR = "Rear"

View File

@ -1,16 +1,22 @@
import logging import logging
from typing import Any
import pysmartthings import pysmartthings
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from pysmartthings import APIResponseError from pysmartthings import APIResponseError
from voluptuous import All, Range
from .const import ( from .const import (
CONF_ENTRY_API_KEY, CONF_ENTRY_API_KEY,
CONF_ENTRY_DEVICE_ID, CONF_ENTRY_DEVICE_ID,
CONF_ENTRY_DEVICE_NAME, CONF_ENTRY_DEVICE_NAME,
CONF_ENTRY_MAX_VOLUME, CONF_ENTRY_MAX_VOLUME,
CONF_ENTRY_SETTINGS_ADVANCED_AUDIO_SWITCHES,
CONF_ENTRY_SETTINGS_EQ_SELECTOR,
CONF_ENTRY_SETTINGS_SOUNDMODE_SELECTOR,
CONF_ENTRY_SETTINGS_WOOFER_NUMBER,
DOMAIN, DOMAIN,
) )
@ -27,24 +33,9 @@ async def validate_input(api, device_id: str):
class ExampleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class ExampleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
_LOGGER.error(f"Example Flow starts with user_input {user_input}")
if user_input is not None: if user_input is not None:
_LOGGER.error(f"User Input is not filled") self.user_input = user_input
try: return await self.async_step_device()
session = async_get_clientsession(self.hass)
api = pysmartthings.SmartThings(
session, user_input.get(CONF_ENTRY_API_KEY)
)
_LOGGER.error(f"Validating Input {user_input}")
device = await validate_input(api, user_input.get(CONF_ENTRY_DEVICE_ID))
_LOGGER.error(
f"Successfully validated Input, Creating entry with title {DOMAIN} and data {user_input}"
)
return self.async_create_entry(title=DOMAIN, data=user_input)
except Exception as excp:
_LOGGER.error(f"Example Flow triggered an exception {excp}")
return self.async_abort(reason="fetch_failed")
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
@ -53,7 +44,98 @@ class ExampleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
vol.Required(CONF_ENTRY_API_KEY): str, vol.Required(CONF_ENTRY_API_KEY): str,
vol.Required(CONF_ENTRY_DEVICE_ID): str, vol.Required(CONF_ENTRY_DEVICE_ID): str,
vol.Required(CONF_ENTRY_DEVICE_NAME): str, vol.Required(CONF_ENTRY_DEVICE_NAME): str,
vol.Required(CONF_ENTRY_MAX_VOLUME): int, vol.Required(CONF_ENTRY_MAX_VOLUME, default=100): All(
int, Range(min=1, max=100)
),
} }
), ),
) )
async def async_step_device(self, user_input: dict[str, any] | None = None):
if user_input is not None:
self.user_input.update(user_input)
try:
session = async_get_clientsession(self.hass)
api = pysmartthings.SmartThings(
session, self.user_input.get(CONF_ENTRY_API_KEY)
)
device = await validate_input(
api, self.user_input.get(CONF_ENTRY_DEVICE_ID)
)
_LOGGER.debug(
f"Successfully validated Input, Creating entry with title {DOMAIN} and data {user_input}"
)
except Exception as excp:
_LOGGER.error(f"The ConfigFlow triggered an exception {excp}")
return self.async_abort(reason="fetch_failed")
return self.async_create_entry(title=DOMAIN, data=self.user_input)
return self.async_show_form(
step_id="device",
data_schema=vol.Schema(
{
vol.Required(CONF_ENTRY_SETTINGS_ADVANCED_AUDIO_SWITCHES): bool,
vol.Required(CONF_ENTRY_SETTINGS_EQ_SELECTOR): bool,
vol.Required(CONF_ENTRY_SETTINGS_SOUNDMODE_SELECTOR): bool,
vol.Required(CONF_ENTRY_SETTINGS_WOOFER_NUMBER): bool,
}
),
)
async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None):
"""Handle a reconfiguration flow initialized by the user."""
self.config_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reconfigure_confirm()
async def async_step_reconfigure_confirm(
self, user_input: dict[str, Any] | None = None
):
"""Handle a reconfiguration flow initialized by the user."""
errors: dict[str, str] = {}
assert self.config_entry
if user_input is not None:
return self.async_update_reload_and_abort(
self.config_entry,
data={**self.config_entry.data, **user_input},
reason="reconfigure_successful",
)
return self.async_show_form(
step_id="reconfigure_confirm",
data_schema=vol.Schema(
{
vol.Required(
CONF_ENTRY_SETTINGS_ADVANCED_AUDIO_SWITCHES,
default=self.config_entry.data.get(
CONF_ENTRY_SETTINGS_ADVANCED_AUDIO_SWITCHES
),
): bool,
vol.Required(
CONF_ENTRY_SETTINGS_EQ_SELECTOR,
default=self.config_entry.data.get(
CONF_ENTRY_SETTINGS_EQ_SELECTOR
),
): bool,
vol.Required(
CONF_ENTRY_SETTINGS_SOUNDMODE_SELECTOR,
default=self.config_entry.data.get(
CONF_ENTRY_SETTINGS_SOUNDMODE_SELECTOR
),
): bool,
vol.Required(
CONF_ENTRY_SETTINGS_WOOFER_NUMBER,
default=self.config_entry.data.get(
CONF_ENTRY_SETTINGS_WOOFER_NUMBER
),
): bool,
vol.Required(CONF_ENTRY_MAX_VOLUME, default=100): All(
int, Range(min=1, max=100)
),
}
),
errors=errors,
)

View File

@ -9,6 +9,12 @@ CONF_ENTRY_API_KEY = "api_key"
CONF_ENTRY_DEVICE_ID = "device_id" CONF_ENTRY_DEVICE_ID = "device_id"
CONF_ENTRY_DEVICE_NAME = "device_name" CONF_ENTRY_DEVICE_NAME = "device_name"
CONF_ENTRY_MAX_VOLUME = "device_volume" CONF_ENTRY_MAX_VOLUME = "device_volume"
CONF_ENTRY_SETTINGS_ADVANCED_AUDIO_SWITCHES = "settings_advanced_audio"
CONF_ENTRY_SETTINGS_EQ_SELECTOR = "settings_eq"
CONF_ENTRY_SETTINGS_SOUNDMODE_SELECTOR = "settings_soundmode"
CONF_ENTRY_SETTINGS_WOOFER_NUMBER = "settings_woofer"
DEFAULT_NAME = DOMAIN DEFAULT_NAME = DOMAIN
BUTTON = BUTTON_DOMAIN BUTTON = BUTTON_DOMAIN

View File

@ -1,12 +1,14 @@
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 .api_extension.SoundbarDevice import SoundbarDevice from .api_extension.SoundbarDevice import SoundbarDevice
from .const import DOMAIN, CONF_ENTRY_DEVICE_ID from .const import CONF_ENTRY_DEVICE_ID, DOMAIN
from .models import DeviceConfig
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -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,12 +1,16 @@
{ {
"domain": "samsung_soundbar", "domain": "samsung_soundbar",
"name": "Samsung Soundbar", "name": "Samsung Soundbar",
"codeowners": ["@samuelspagl"], "codeowners": [
"@samuelspagl"
],
"config_flow": true, "config_flow": true,
"documentation": "https://www.example.com", "documentation": "https://www.example.com",
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"issue_tracker": "https://github.com/samuelspagl/ha_samsung_soundbar/issues", "issue_tracker": "https://github.com/samuelspagl/ha_samsung_soundbar/issues",
"requirements": ["pysmartthings"], "requirements": [
"version": "0.1.0" "pysmartthings==0.7.8"
} ],
"version": "0.4.1"
}

View File

@ -1,26 +1,18 @@
import logging import logging
from typing import Any, Mapping
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
DEVICE_CLASS_SPEAKER, DEVICE_CLASS_SPEAKER,
MediaPlayerEntity, MediaPlayerEntity,
) )
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import MediaPlayerEntityFeature
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_SELECT_SOUND_MODE,
SUPPORT_SELECT_SOURCE,
SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import DeviceInfo, generate_entity_id from homeassistant.helpers.entity import DeviceInfo, generate_entity_id
from homeassistant.helpers import config_validation as cv, entity_platform, selector
import voluptuous as vol
from .models import DeviceConfig
from .api_extension.SoundbarDevice import SoundbarDevice from .api_extension.SoundbarDevice import SoundbarDevice
from .api_extension.const import SpeakerIdentifier, RearSpeakerMode
from .const import ( from .const import (
CONF_ENTRY_API_KEY, CONF_ENTRY_API_KEY,
CONF_ENTRY_DEVICE_ID, CONF_ENTRY_DEVICE_ID,
@ -28,6 +20,7 @@ from .const import (
CONF_ENTRY_MAX_VOLUME, CONF_ENTRY_MAX_VOLUME,
DOMAIN, DOMAIN,
) )
from .models import DeviceConfig
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -35,22 +28,90 @@ DEFAULT_NAME = "SmartThings Soundbar"
CONF_MAX_VOLUME = "max_volume" CONF_MAX_VOLUME = "max_volume"
SUPPORT_SMARTTHINGS_SOUNDBAR = ( SUPPORT_SMARTTHINGS_SOUNDBAR = (
SUPPORT_PAUSE MediaPlayerEntityFeature.PAUSE
| SUPPORT_VOLUME_STEP | MediaPlayerEntityFeature.VOLUME_STEP
| SUPPORT_VOLUME_MUTE | MediaPlayerEntityFeature.VOLUME_MUTE
| SUPPORT_VOLUME_SET | MediaPlayerEntityFeature.VOLUME_SET
| SUPPORT_SELECT_SOURCE | MediaPlayerEntityFeature.SELECT_SOURCE
| SUPPORT_TURN_OFF | MediaPlayerEntityFeature.TURN_OFF
| SUPPORT_TURN_ON | MediaPlayerEntityFeature.TURN_ON
| SUPPORT_PLAY | MediaPlayerEntityFeature.PLAY
| SUPPORT_STOP | MediaPlayerEntityFeature.NEXT_TRACK
| SUPPORT_SELECT_SOUND_MODE | MediaPlayerEntityFeature.PREVIOUS_TRACK
| MediaPlayerEntityFeature.STOP
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
) )
def addServices():
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
"select_soundmode",
cv.make_entity_service_schema({vol.Required("sound_mode"): str}),
SmartThingsSoundbarMediaPlayer.async_select_sound_mode.__name__,
)
platform.async_register_entity_service(
"set_woofer_level",
cv.make_entity_service_schema(
{vol.Required("level"): vol.All(int, vol.Range(min=-12, max=6))}
),
SmartThingsSoundbarMediaPlayer.async_set_woofer_level.__name__,
)
platform.async_register_entity_service(
"set_night_mode",
cv.make_entity_service_schema({vol.Required("enabled"): bool}),
SmartThingsSoundbarMediaPlayer.async_set_night_mode.__name__,
)
platform.async_register_entity_service(
"set_bass_enhancer",
cv.make_entity_service_schema({vol.Required("enabled"): bool}),
SmartThingsSoundbarMediaPlayer.async_set_bass_mode.__name__,
)
platform.async_register_entity_service(
"set_voice_enhancer",
cv.make_entity_service_schema({vol.Required("enabled"): bool}),
SmartThingsSoundbarMediaPlayer.async_set_voice_mode.__name__,
)
platform.async_register_entity_service(
"set_speaker_level",
cv.make_entity_service_schema(
{vol.Required("speaker_identifier"): str, vol.Required("level"): int}
),
SmartThingsSoundbarMediaPlayer.async_set_speaker_level.__name__,
)
platform.async_register_entity_service(
"set_rear_speaker_mode",
cv.make_entity_service_schema({vol.Required("speaker_mode"): str}),
SmartThingsSoundbarMediaPlayer.async_set_rear_speaker_mode.__name__,
)
platform.async_register_entity_service(
"set_active_voice_amplifier",
cv.make_entity_service_schema({vol.Required("enabled"): bool}),
SmartThingsSoundbarMediaPlayer.async_set_active_voice_amplifier.__name__,
)
platform.async_register_entity_service(
"set_space_fit_sound",
cv.make_entity_service_schema({vol.Required("enabled"): bool}),
SmartThingsSoundbarMediaPlayer.async_set_space_fit_sound.__name__,
)
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
domain_data = hass.data[DOMAIN] domain_data = hass.data[DOMAIN]
addServices()
entities = [] entities = []
for key in domain_data.devices: for key in domain_data.devices:
device_config: DeviceConfig = domain_data.devices[key] device_config: DeviceConfig = domain_data.devices[key]
@ -187,5 +248,47 @@ class SmartThingsSoundbarMediaPlayer(MediaPlayerEntity):
async def async_media_pause(self): async def async_media_pause(self):
await self.device.media_pause() await self.device.media_pause()
async def async_media_next_track(self):
await self.device.media_next_track()
async def async_media_previous_track(self):
await self.device.media_previous_track()
async def async_media_stop(self): async def async_media_stop(self):
await self.device.media_stop() await self.device.media_stop()
# ---------- SERVICE_UTILITY ------------
async def async_set_woofer_level(self, level: int):
await self.device.set_woofer(level)
async def async_set_bass_mode(self, enabled: bool):
await self.device.set_bass_mode(enabled)
async def async_set_voice_mode(self, enabled: bool):
await self.device.set_voice_amplifier(enabled)
async def async_set_night_mode(self, enabled: bool):
await self.device.set_night_mode(enabled)
# ---------- SERVICE_UTILITY ------------
async def async_set_speaker_level(self, speaker_identifier: str, level: int):
await self.device.set_speaker_level(
SpeakerIdentifier(speaker_identifier), level
)
async def async_set_rear_speaker_mode(self, speaker_mode: str):
await self.device.set_rear_speaker_mode(RearSpeakerMode(speaker_mode))
async def async_set_active_voice_amplifier(self, enabled: bool):
await self.device.set_active_voice_amplifier(enabled)
async def async_set_space_fit_sound(self, enabled: bool):
await self.device.set_space_fit_sound(enabled)
# This property can be uncommented for some extra_attributes
# Still enabling this can cause side-effects.
# @property
# def extra_state_attributes(self) -> Mapping[str, Any] | None:
# return {"device_information": self.device.retrieve_data}

View File

@ -1,11 +1,15 @@
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 .api_extension.SoundbarDevice import SoundbarDevice from .api_extension.SoundbarDevice import SoundbarDevice
from .const import CONF_ENTRY_DEVICE_ID, DOMAIN from .const import CONF_ENTRY_DEVICE_ID, CONF_ENTRY_SETTINGS_WOOFER_NUMBER, DOMAIN
from .models import DeviceConfig
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -17,31 +21,34 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for key in domain_data.devices: for key in domain_data.devices:
device_config: DeviceConfig = domain_data.devices[key] device_config: DeviceConfig = domain_data.devices[key]
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
) and config_entry.data.get(CONF_ENTRY_SETTINGS_WOOFER_NUMBER):
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 +58,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,189 @@
import logging
from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.helpers.entity import DeviceInfo
from .api_extension.SoundbarDevice import SoundbarDevice
from .const import (
CONF_ENTRY_DEVICE_ID,
CONF_ENTRY_SETTINGS_EQ_SELECTOR,
CONF_ENTRY_SETTINGS_SOUNDMODE_SELECTOR,
DOMAIN,
)
from .models import DeviceConfig
_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):
if config_entry.data.get(CONF_ENTRY_SETTINGS_EQ_SELECTOR):
entities.append(
EqPresetSelectEntity(device, "eq_preset", "mdi:tune-vertical")
)
if config_entry.data.get(CONF_ENTRY_SETTINGS_SOUNDMODE_SELECTOR):
entities.append(
SoundModeSelectEntity(
device, "sound_mode_preset", "mdi:surround-sound"
)
)
entities.append(
InputSelectEntity(device, "input_preset", "mdi:video-input-hdmi")
)
async_add_entities(entities)
return True
class EqPresetSelectEntity(SelectEntity):
def __init__(
self,
device: SoundbarDevice,
append_unique_id: str,
icon_string: str,
):
self.entity_id = f"number.{device.device_name}_{append_unique_id}"
self.entity_description = SelectEntityDescription(
key=append_unique_id,
)
self.__base_icon = icon_string
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
@property
def icon(self) -> str | None:
return self.__base_icon
# ------ 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,
icon_string: str,
):
self.entity_id = f"number.{device.device_name}_{append_unique_id}"
self.entity_description = SelectEntityDescription(
key=append_unique_id,
)
self.__base_icon = icon_string
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
@property
def icon(self) -> str | None:
return self.__base_icon
# ------ 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,
icon_string: str,
):
self.entity_id = f"number.{device.device_name}_{append_unique_id}"
self.entity_description = SelectEntityDescription(
key=append_unique_id,
)
self.__base_icon = icon_string
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
@property
def icon(self) -> str | None:
return self.__base_icon
# ------ 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)

View File

@ -0,0 +1,56 @@
import logging
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.helpers.entity import DeviceInfo
from .api_extension.SoundbarDevice import SoundbarDevice
from .const import CONF_ENTRY_DEVICE_ID, DOMAIN
from .models import DeviceConfig
_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(VolumeSensor(device, "volume_level", "mdi:volume-high"))
async_add_entities(entities)
return True
class VolumeSensor(SensorEntity):
def __init__(self, device: SoundbarDevice, append_unique_id: str, icon_string: str):
self.entity_id = f"sensor.{device.device_name}_{append_unique_id}"
self.__device = device
self._attr_unique_id = f"{device.device_id}_sw_{append_unique_id}"
self.__base_icon = icon_string
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
_attr_device_class = SensorDeviceClass.VOLUME
@property
def icon(self) -> str | None:
return self.__base_icon
def update(self) -> None:
"""Fetch new state data for the sensor.
This is the only method that should fetch new data for Home Assistant.
"""
self._attr_native_value = self.__device.device.status.volume

View File

@ -0,0 +1,167 @@
select_soundmode:
name: Select Soundmode
description: Some Soundbars support different "sound modes". If supported you can select them here.
target:
device:
integration: samsung_soundbar
fields:
sound_mode:
name: Sound Mode
description: Select the Soundmode you are interested in.
required: true
example: "adaptive sound"
# The default field value
default: "standard"
# Selector (https://www.home-assistant.io/docs/blueprint/selectors/) to control
# the input UI for this field
selector:
select:
translation_key: "soundmode"
options:
- "standard"
- "surround"
- "game"
- "adaptive sound"
set_woofer_level:
name: Set Woofer level
description: Set the subwoofer level of your soundbar
target:
device:
integration: samsung_soundbar
fields:
level:
name: Volume level
required: true
example: 3
default: 0
selector:
number:
min: -12
max: 6
step: 1
set_night_mode:
name: Set NightMode
description: Activates / deactivates the Nightmode
target:
device:
integration: samsung_soundbar
fields:
enabled:
name: Enabled / Disabled
required: true
example: true
default: false
selector:
boolean:
set_bass_enhancer:
name: Set bass enhancement
description: Activates / deactivates the bass enhancement
target:
device:
integration: samsung_soundbar
fields:
enabled:
name: Enabled / Disabled
required: true
example: true
default: false
selector:
boolean:
set_voice_enhancer:
name: Set voice enhancement
description: Activates / deactivates the voice enhancement
target:
device:
integration: samsung_soundbar
fields:
enabled:
name: Enabled / Disabled
required: true
example: true
default: false
selector:
boolean:
set_speaker_level:
name: Set Speaker level
description: Set the speaker levels of your soundbar
target:
device:
integration: samsung_soundbar
fields:
speaker_identifier:
name: Speaker Identifier
required: true
example: Spk_Center
selector:
select:
translation_key: "speaker_identifier"
options:
- "Spk_Center"
- "Spk_Side"
- "Spk_Wide"
- "Spk_Front_Top"
- "Spk_Rear"
- "Spk_Rear_Top"
level:
name: Speaker Level
required: true
example: 0
selector:
number:
min: -6
max: 6
step: 1
set_rear_speaker_mode:
name: Set rear speaker mode
description: Set the rear speaker mode of your soundbar
target:
device:
integration: samsung_soundbar
fields:
speaker_mode:
name: Speaker mode
required: true
example: Rear
selector:
select:
translation_key: "rear_speaker_mode"
options:
- "Rear"
- "Front"
set_active_voice_amplifier:
name: Set active voice amplifier
description: Activates / deactivates the active voice amplifier
target:
device:
integration: samsung_soundbar
fields:
enabled:
name: Enabled / Disabled
required: true
example: true
default: false
selector:
boolean:
set_space_fit_sound:
name: Set SpaceFitSound
description: Activates / deactivates the SpaceFitSound
target:
device:
integration: samsung_soundbar
fields:
enabled:
name: Enabled / Disabled
required: true
example: true
default: false
selector:
boolean:

View File

@ -3,9 +3,13 @@ import logging
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from .models import DeviceConfig
from .api_extension.SoundbarDevice import SoundbarDevice from .api_extension.SoundbarDevice import SoundbarDevice
from .const import CONF_ENTRY_DEVICE_ID, DOMAIN from .const import (
CONF_ENTRY_DEVICE_ID,
CONF_ENTRY_SETTINGS_ADVANCED_AUDIO_SWITCHES,
DOMAIN,
)
from .models import DeviceConfig
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -18,33 +22,37 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
device_config: DeviceConfig = domain_data.devices[key] device_config: DeviceConfig = domain_data.devices[key]
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( if config_entry.data.get(CONF_ENTRY_SETTINGS_ADVANCED_AUDIO_SWITCHES):
SoundbarSwitchAdvancedAudio( entities.append(
device, SoundbarSwitchAdvancedAudio(
"nightmode", device,
lambda: device.night_mode, "nightmode",
device.set_night_mode, lambda: device.night_mode,
device.set_night_mode, device.set_night_mode,
device.set_night_mode,
"mdi:weather-night",
)
) )
) entities.append(
entities.append( SoundbarSwitchAdvancedAudio(
SoundbarSwitchAdvancedAudio( device,
device, "bassmode",
"bassmode", lambda: device.bass_mode,
lambda: device.bass_mode, device.set_bass_mode,
device.set_bass_mode, device.set_bass_mode,
device.set_bass_mode, "mdi:speaker-wireless",
)
) )
) entities.append(
entities.append( SoundbarSwitchAdvancedAudio(
SoundbarSwitchAdvancedAudio( device,
device, "voice_amplifier",
"voice_amplifier", lambda: device.voice_amplifier,
lambda: device.voice_amplifier, device.set_voice_amplifier,
device.set_voice_amplifier, device.set_voice_amplifier,
device.set_voice_amplifier, "mdi:account-voice",
)
) )
)
async_add_entities(entities) async_add_entities(entities)
return True return True
@ -57,12 +65,14 @@ class SoundbarSwitchAdvancedAudio(SwitchEntity):
state_function, state_function,
on_function, on_function,
off_function, off_function,
icon_string: str = "mdi:toggle-switch-variant",
): ):
self.entity_id = f"switch.{device.device_name}_{append_unique_id}" self.entity_id = f"switch.{device.device_name}_{append_unique_id}"
self.__device = device self.__device = device
self._name = f"{self.__device.device_name} {append_unique_id}" self._name = f"{self.__device.device_name} {append_unique_id}"
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.__base_icon = icon_string
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.__device.device_id)}, identifiers={(DOMAIN, self.__device.device_id)},
name=self.__device.device_name, name=self.__device.device_name,
@ -85,6 +95,10 @@ class SoundbarSwitchAdvancedAudio(SwitchEntity):
def update(self): def update(self):
self.__state = self.__state_function() self.__state = self.__state_function()
@property
def icon(self) -> str | None:
return self.__base_icon
# ------ STATE FUNCTIONS -------- # ------ STATE FUNCTIONS --------
@property @property
def state(self): def state(self):

View File

@ -10,7 +10,144 @@
}, },
"description": "Bitte gib deine Daten ein.", "description": "Bitte gib deine Daten ein.",
"title": "Authentifizierung" "title": "Authentifizierung"
},
"device":{
"data" : {
"settings_advanced_audio": "'Advanced Audio switches' aktivieren (NightMode, BassMode, VoiceEnhancer)",
"settings_eq": "'EQ selector' aktivieren",
"settings_soundmode": "'Soundmode selector' aktivieren",
"settings_woofer": "'Subwoofer Entität' aktivieren"
},
"description": "Einige Soundbars haben verschiedene Featuresets. Wähle bitte aus welche Features von deiner Soundbar supported werden (einsehbar in der SmartThings App).",
"title": "Geräte Einstellungen"
},
"reconfigure_confirm":{
"data" : {
"settings_advanced_audio": "'Advanced Audio switches' aktivieren (NightMode, BassMode, VoiceEnhancer)",
"settings_eq": "'EQ selector' aktivieren",
"settings_soundmode": "'Soundmode selector' aktivieren",
"settings_woofer": "'Subwoofer Entität' aktivieren",
"device_volume": "Max Volume (int)"
},
"description": "Einige Soundbars haben verschiedene Featuresets. Wähle bitte aus welche Features von deiner Soundbar supported werden (einsehbar in der SmartThings App).",
"title": "Geräte Einstellungen"
}
}
},
"selector": {
"soundmode": {
"options": {
"standard": "Standard",
"surround": "Surround",
"game": "Gaming",
"adaptive sound": "Adaptive Sound"
}
},
"speaker_identifier": {
"options": {
"Spk_Center": "Center",
"Spk_Side": "Side",
"Spk_Wide": "Wide",
"Spk_Front_Top": "Front Top",
"Spk_Rear": "Rear",
"Spk_Rear_Top": "Rear Top"
}
},
"rear_speaker_mode": {
"options": {
"Rear": "Rear",
"Front": "Front"
}
}
},
"services":{
"select_soundmode":{
"name": "SoundMode auswählen",
"description": "Wähle hier zwischen, 'Standard', 'Surround', 'Game' und 'Adaptive Sound'."
},
"set_woofer_level":{
"name": "Subwoofer Level setzen",
"description": "Verändere die Lautstärke deines Subwoofers.",
"fields":{
"level":{
"name": "Volume Level",
"description": "Subwoofer Level, von -12 bis +6"
}
}
},
"set_night_mode":{
"name": "Nachtmodus setzen",
"description": "Schalte den 'Nachtmodus' an / aus.",
"fields":{
"enabled":{
"name": "An / ausschalten",
"description": "Siehe Name."
}
}
},
"set_bass_enhancer":{
"name": "Bassmodus setzen",
"description": "Schalte den 'Bassmodus' an / aus.",
"fields":{
"enabled":{
"name": "An / ausschalten",
"description": "Siehe Name."
}
}
},
"set_voice_enhancer":{
"name": "Stimmenverbesserer setzen",
"description": "Schalte den 'Stimmenverbesserer' an / aus.",
"fields":{
"enabled":{
"name": "An / ausschalten",
"description": "Siehe Name."
}
}
},
"set_speaker_level":{
"name": "Lautsprecher level verändern",
"description": "Verändere die Lautstärke der einzelnen Lautsprecher",
"fields":{
"speaker_identifier": {
"name": "Lautsprecher",
"description": "Auszuwählender Lautsprecher"
},
"level": {
"name": "Lautstärke Level",
"description": "Lautstärke Level zwischen -6 und 6."
}
}
},
"set_rear_speaker_mode":{
"name": "Modus der hinteren Lautsprecher setzen",
"description": "Nutze deine Rücklautsprecher, als 'Vorder-' oder 'Rücklautsprecher'.",
"fields":{
"speaker_mode": {
"name": "Lautsprecher Modus",
"description": "Nutze den Lautsprecher als Front oder Rear Speaker."
}
}
},
"set_active_voice_amplifier":{
"name": "Stimmenverstärker setzen",
"description": "Schalte den 'Stimmenverstärker' an / aus.",
"fields":{
"enabled":{
"name": "An / ausschalten",
"description": "Siehe Name."
}
}
},
"set_space_fit_sound":{
"name": "SpaceFitSound setzen",
"description": "Schalte den 'SpaceFitSound' an / aus.",
"fields":{
"enabled":{
"name": "An / ausschalten",
"description": "Siehe Name."
} }
} }
} }
}
} }

View File

@ -1,15 +1,152 @@
{ {
"config":{ "config": {
"step":{ "step": {
"user":{ "user": {
"data": { "data": {
"api_key": "SmartThings API Token", "api_key": "SmartThings API Token",
"device_id": "Device ID", "device_id": "Device ID",
"device_name":"Device Name", "device_name": "Device Name",
"device_volume": "Max Volume (int)" "device_volume": "Max Volume (int)"
}, },
"description": "Please enter your credentials.", "description": "Please enter your credentials.",
"title": "Authentication" "title": "Authentication"
},
"device": {
"data": {
"settings_advanced_audio": "Enable 'Advanced Audio switches' capabilities (NightMode, BassMode, VoiceEnhancer)",
"settings_eq": "Enable 'EQ selector' capabilities",
"settings_soundmode": "Enable 'Soundmode selector' capabilities",
"settings_woofer": "Enable 'Woofer number' capability"
},
"description": "Some soundbars have a different featureset than others. Please the features supported by your soundbar (visible in the SmartThings App).",
"title": "Device Settings"
},
"reconfigure_confirm": {
"data": {
"settings_advanced_audio": "Enable 'Advanced Audio switches' capabilities (NightMode, BassMode, VoiceEnhancer)",
"settings_eq": "Enable 'EQ selector' capabilities",
"settings_soundmode": "Enable 'Soundmode selector' capabilities",
"settings_woofer": "Enable 'Woofer number' capability",
"device_volume": "Max Volume (int)"
},
"description": "Some soundbars have a different featureset than others. Please the features supported by your soundbar (visible in the SmartThings App).",
"title": "Device Settings"
}
}
},
"selector": {
"soundmode": {
"options": {
"standard": "Standard",
"surround": "Surround",
"game": "Gaming",
"adaptive sound": "Adaptive Sound"
}
},
"speaker_identifier": {
"options": {
"Spk_Center": "Center",
"Spk_Side": "Side",
"Spk_Wide": "Wide",
"Spk_Front_Top": "Front Top",
"Spk_Rear": "Rear",
"Spk_Rear_Top": "Rear Top"
}
},
"rear_speaker_mode": {
"options": {
"Rear": "Rear",
"Front": "Front"
}
}
},
"services": {
"select_soundmode": {
"name": "Select Sound Mode",
"description": "Choose between 'Standard', 'Surround', 'Game', and 'Adaptive Sound'."
},
"set_woofer_level": {
"name": "Set Subwoofer Level",
"description": "Change the volume of your subwoofer.",
"fields": {
"level": {
"name": "Volume Level",
"description": "Subwoofer level, from -12 to +6"
}
}
},
"set_night_mode": {
"name": "Set Night Mode",
"description": "Turn 'Night Mode' on/off.",
"fields": {
"enabled": {
"name": "On/Off",
"description": "See name."
}
}
},
"set_bass_enhancer": {
"name": "Set Bass Mode",
"description": "Turn 'Bass Mode' on/off.",
"fields": {
"enabled": {
"name": "On/Off",
"description": "See name."
}
}
},
"set_voice_enhancer": {
"name": "Set Voice Enhancer",
"description": "Turn 'Voice Enhancer' on/off.",
"fields": {
"enabled": {
"name": "On/Off",
"description": "See name."
}
}
},
"set_speaker_level": {
"name": "Change Speaker Level",
"description": "Change the volume of individual speakers.",
"fields":{
"speaker_identifier": {
"name": "Speaker Identifier",
"description": "Identifier of the speaker."
},
"level": {
"name": "Level",
"description": "Level of the Speaker from -6 to 6."
}
}
},
"set_rear_speaker_mode": {
"name": "Set Rear Speaker Mode",
"description": "Use your rear speakers as 'Front' or 'Rear' speakers.",
"fields":{
"speaker_mode": {
"name": "Speaker mode",
"description": "Weather the speaker are used as rear / front speakers."
}
}
},
"set_active_voice_amplifier": {
"name": "Set Voice Amplifier",
"description": "Turn 'Voice Amplifier' on/off.",
"fields": {
"enabled": {
"name": "On/Off",
"description": "See name."
}
}
},
"set_space_fit_sound": {
"name": "Set SpaceFitSound",
"description": "Turn 'SpaceFitSound' on/off.",
"fields": {
"enabled": {
"name": "On/Off",
"description": "See name."
}
} }
} }
} }

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: 'Yet another Samsung Soundbar integration for Home Assistant',
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: false,
showLinkIcon: true,
exclude: [],
fluid: true
}
}
})

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

@ -0,0 +1,70 @@
---
title: "YASSI"
---
::block-hero
---
cta:
- Why another HomeAssistant integration?
- /first-things-first/why-another-integration
secondary:
- Open on GitHub →
- https://github.com/samuelspagl/ha_samsung_soundbar
---
#title
Yassi - Yet another Samsung Soundbar integration
#description
**YASSI** is a **HomeAssistant** integration for **Samsung Soundbars**. It enhances control, and adds features like equalizer settings. Install it via HACS or manually. Kudos to the original idea by @PiotrMachowski and @thierryBourbon! 🎶🔊
[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=samuelspagl&repository=ha_samsung_soundbar&category=integration)
::
::card-grid
#title
Quick-Start
#root
:ellipsis
#default
::card
#title
❓ Why another integration?
#description
Whether you thought about it or not, here is the answer ;).
<br>
<br>
:button-link[click here]{href="/first-things-first/why-another-integration"}
::
::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="/first-things-first/getting-started"}
::
::card
#title
✨ Features
#description
Many cool features are awaiting your eyes to see ✨.
<br>
<br>
:button-link[click here]{href="/features"}
::
::card
#title
⚙️ SmartThings API related information
#description
If you want to know some background information on how equalizer support and
other things were implemented, this is your section.
<br>
<br>
:button-link[click here]{href="/features"}
::
::

View File

@ -0,0 +1,13 @@
# ❓ 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,43 @@
# 🚀 Getting Started with Yassi
Welcome to Yassi, the HomeAssistant integration for your Samsung Soundbar. This guide will help you get up and running in no time.
## 📦 Installation Options
### HACS (Home Assistant Community Store)
#### 🌟 Official Repository (Coming Soon)
The Yassi integration will be available through the official HACS repository shortly. Stay tuned for updates.
#### Custom Repository
In the meantime, you can manually add this repository to HACS:
1. Click the following button and 'open link':
[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=samuelspagl&repository=ha_samsung_soundbar&category=integration)
2. Click 'add' to add the custom repository.
3. Download 'Yassi' and restart Home Assistant.
### 📂 Manual Installation
If you prefer to install Yassi manually:
1. Download the latest release from the repository.
2. Extract and copy the `custom_components/samsung_soundbar` folder.
3. Paste it into the `config/custom_components/samsung_soundbar` directory of your HomeAssistant setup.
## ⚙️ Configuration Steps
Once Yassi is installed, you can configure it via the HomeAssistant UI:
1. Go to 'Configuration' and then 'Integrations'.
2. Click on 'Add Integration' and search for 'Yassi'.
3. Enter the following details to complete the setup:
- 🔑 SmartThings API Key: [Obtain it here](https://account.smartthings.com/tokens).
- 🆔 Device ID: [Find your Soundbar's device ID here](https://my.smartthings.com/advanced/devices).
- ㍻ Soundbar Name: Choose a name for easy identification.
- 🔊 Max Volume: Set the maximum volume limit for your Soundbar.
Follow these steps, and you'll be enjoying seamless control over your Samsung Soundbar with Yassi in no time!
::alert{type="info"}
The `🔊 Max Volume` setting will readjust the internal values of the `media_player` entity from 0-100 to 0-MaxVolume.
Therefore will the slider not display the same value as the one provided by the `sensor` entity, which will always display
the raw value retrieved from the SmartThings API.
::

View File

@ -0,0 +1,17 @@
# ‼️ Issues and other things
As the creator of this personal and fun project, I am thrilled to see people using it. While I wont always have immediate availability to address every request, Ill do my best to fix issues and implement features. Thanks a lot in advance! 🙌
Here are some best practices to help me help you:
1. 🐞 GitHub Issues: For any issues or bugs, please submit them via GitHub Issues. ([🔗 click here](https://github.com/samuelspagl/ha_samsung_soundbar/issues/new))
2. 📋 Provide Details: Include essential information:
- Home Assistant OS Version
- Samsung Soundbar Model
- Other Relevant Details (like debug logs)
3. 🎇 Icons for Fun:
- 📦 = Feature Request
- 🐛 = Bug Report
- ❓ = General Questions
Lets collaborate to enhance your soundbar experience! 🎶🔊

View File

@ -0,0 +1,52 @@
# ✨ Features Overview
Yassi allows you to retrieve and set the status of various features on your Samsung Soundbar. Below is a breakdown of capabilities organized by entity type.
## `media_player` Entity
| **Feature** | **Capability** | **Access Type** |
|-------------------|----------------|-----------------|
| Power | on / off | Read, Write |
| Volume | set, step | Read, Write |
| Input Selection | select | Read, Write |
| Sound Mode | select | Read, Write |
| Playback Control | play, pause | Write |
| Media Information | artwork, title, artist | Read |
## `number` Entity
| **Feature** | **Capability** | **Access Type** |
|-------------|----------------|-----------------|
| Woofer Level | set | Read, Write |
## `select` Entity
| **Feature** | **Capability** | **Access Type** |
|-------------------|-----------------------|-----------------|
| Input | input, supported_inputs | Read, Write |
| Sound Mode | active_soundmode, supported_soundmodes | Read, Write |
| EQ-Preset | active_eq_preset, supported_eq_preset | Read, Write |
## `button` Entity
| **Feature** | **Capability** | **Access Type** |
|-------------------|----------------|-----------------|
| Night Mode | toggle | Read, Write |
| Voice Amplifier | toggle | Read, Write |
| Bass Mode | toggle | Read, Write |
## `image` Entity
| **Feature** | **Capability** | **Access Type** |
|-------------------|----------------|-----------------|
| Media Cover Art | display | Read |
## `sensor` Entity
| **Feature** | **Capability** | **Access Type** |
|-------------|--------------------|-----------------|
| Volume | float sensor value | Read |
Hopefully this format provides a clear and concise view of what Yassi can do with your Samsung Soundbar, making it easier to understand and configure.

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>

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

@ -0,0 +1,10 @@
export default defineNuxtConfig({
// https://github.com/nuxt-themes/docus
extends: '@nuxt-themes/docus',
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

View File

@ -2,5 +2,6 @@
"name": "Samsung Soundbar", "name": "Samsung Soundbar",
"filename": "samsung_soundbar.zip", "filename": "samsung_soundbar.zip",
"render_readme": true, "render_readme": true,
"zip_release": true "zip_release": true,
"homeassistant": "2024.3.0"
} }

20
scripts/develop Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
# Create config dir if not present
if [[ ! -d "${PWD}/config" ]]; then
mkdir -p "${PWD}/config"
hass --config "${PWD}/config" --script ensure_config
fi
# Set the path to custom_components
## This let's us have the structure we want <root>/custom_components/integration_blueprint
## while at the same time have Home Assistant configuration inside <root>/config
## without resulting to symlinks.
export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
# Start Home Assistant
hass --config "${PWD}/config" --debug

7
scripts/setup Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
pip install rich pysmartthings