🚀: Initial Commit

This commit is contained in:
samuelspagl 2023-09-06 16:53:22 +02:00
parent d96323fd81
commit 0f43da8f1e
22 changed files with 2508 additions and 0 deletions

29
.github/workflows/hacs.yaml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Validate HACS
on:
push:
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
name: Download repo
with:
fetch-depth: 0
- uses: actions/setup-python@v2
name: Setup Python
with:
python-version: '3.8.x'
- uses: actions/cache@v2
name: Cache
with:
path: |
~/.cache/pip
key: custom-component-ci
- name: HACS Action
uses: hacs/action@main
with:
CATEGORY: integration

12
.github/workflows/hassfest.yaml vendored Normal file
View File

@ -0,0 +1,12 @@
name: Validate with hassfest
on:
push:
pull_request:
jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v2"
- uses: home-assistant/actions/hassfest@master

27
.github/workflows/release.yaml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Release
on:
release:
types: [published]
jobs:
release:
name: Prepare release
runs-on: ubuntu-latest
steps:
- name: Download repo
uses: actions/checkout@v1
- name: Zip samsung_soundbar dir
run: |
cd /home/runner/work/ha_samsung_soundbar/ha_samsung_soundbar/custom_components/samsung_soundbar
zip samsung_soundbar.zip -r ./
- name: Upload zip to release
uses: svenstaro/upload-release-action@v1-release
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: /home/runner/work/ha_samsung_soundbar/ha_samsung_soundbar/custom_components/samsung_soundbar/samsung_soundbar.zip
asset_name: samsung_soundbar.zip
tag: ${{ github.ref }}
overwrite: true

18
.github/workflows/validate.yaml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Validate
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
validate-hacs:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- name: HACS validation
uses: "hacs/action@main"
with:
category: "integration"

161
.gitignore vendored Normal file
View File

@ -0,0 +1,161 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
.idea
.pycharm
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

16
Pipfile Normal file
View File

@ -0,0 +1,16 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
pysmartthings = "*"
rich = "*"
homeassistant = "*"
[dev-packages]
black = "*"
isort = "*"
[requires]
python_version = "3.11"

1224
Pipfile.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

View File

@ -0,0 +1,75 @@
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import DOMAIN, HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from pysmartthings import SmartThings
from .api_extension.SoundbarDevice import SoundbarDevice
from .const import (
CONF_ENTRY_API_KEY,
CONF_ENTRY_DEVICE_ID,
CONF_ENTRY_DEVICE_NAME,
CONF_ENTRY_MAX_VOLUME,
SUPPORTED_DOMAINS,
DOMAIN,
)
from .models import DeviceConfig, SoundbarConfig
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["media_player", "switch", "image", "number"]
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."""
# store shell object
_LOGGER.info(f"[{DOMAIN}] Starting to setup ConfigEntry {entry.data}")
if not DOMAIN in hass.data:
_LOGGER.info(f"[{DOMAIN}] Domain not found in hass.data setting default")
hass.data[DOMAIN] = SoundbarConfig(
SmartThings(
async_get_clientsession(hass), entry.data.get(CONF_ENTRY_API_KEY)
),
{},
)
domain_config: SoundbarConfig = hass.data[DOMAIN]
_LOGGER.info(f"[{DOMAIN}] Retrieved Domain Config: {domain_config}")
if not entry.data.get(CONF_ENTRY_DEVICE_ID) in domain_config.devices:
_LOGGER.info(
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(
entry.data.get(CONF_ENTRY_DEVICE_ID)
)
session = async_get_clientsession(hass)
soundbar_device = SoundbarDevice(
smart_things_device,
session,
entry.data.get(CONF_ENTRY_MAX_VOLUME),
entry.data.get(CONF_ENTRY_DEVICE_NAME),
)
await soundbar_device.update()
domain_config.devices[entry.data.get(CONF_ENTRY_DEVICE_ID)] = DeviceConfig(
entry.data,
soundbar_device
)
_LOGGER.info(f"[{DOMAIN}] after initializing Soundbar device")
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
domain_data = hass.data[DOMAIN]
if unload_ok:
del domain_data.devices[entry.data.get(CONF_ENTRY_DEVICE_ID)]
if len(domain_data.devices) == 0:
del hass.data[DOMAIN]
return unload_ok

View File

@ -0,0 +1,382 @@
import json
import time
from urllib.parse import quote
from pysmartthings import DeviceEntity
class SoundbarDevice:
def __init__(
self, device: DeviceEntity, session, max_volume: int, device_name: str
):
self.device = device
self._device_id = self.device.device_id
self._api_key = self.device._api.token
self.__session = session
self.__device_name = device_name
self.__supported_soundmodes = []
self.__active_soundmode = ""
self.__woofer_level = 0
self.__woofer_connection = ""
self.__active_eq_preset = ""
self.__supported_eq_presets = []
self.__eq_action = ""
self.__eq_bands = []
self.__voice_amplifier = 0
self.__night_mode = 0
self.__bass_mode = 0
self.__media_title = ""
self.__media_artist = ""
self.__media_cover_url = ""
self.__old_media_title = ""
self.__max_volume = max_volume
async def update(self):
await self.device.status.refresh()
await self._update_media()
await self._update_soundmode()
await self._update_advanced_audio()
await self._update_woofer()
await self._update_equalizer()
async def _update_media(self):
self.__media_artist = self.device.status._attributes["audioTrackData"].value[
"artist"
]
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
self.__media_cover_url = await self.get_song_title_artwork(
self.__media_artist, self.__media_title
)
async def _update_soundmode(self):
await self.update_execution_data(["/sec/networkaudio/soundmode"])
payload = await self.get_execute_status()
self.__supported_soundmodes = payload[
"x.com.samsung.networkaudio.supportedSoundmode"
]
self.__active_soundmode = payload["x.com.samsung.networkaudio.soundmode"]
async def _update_woofer(self):
await self.update_execution_data(["/sec/networkaudio/woofer"])
payload = await self.get_execute_status()
self.__woofer_level = payload["x.com.samsung.networkaudio.woofer"]
self.__woofer_connection = payload["x.com.samsung.networkaudio.connection"]
async def _update_equalizer(self):
await self.update_execution_data(["/sec/networkaudio/eq"])
payload = await self.get_execute_status()
self.__active_eq_preset = payload["x.com.samsung.networkaudio.EQname"]
self.__supported_eq_presets = payload[
"x.com.samsung.networkaudio.supportedList"
]
self.__eq_action = payload["x.com.samsung.networkaudio.action"]
self.__eq_bands = payload["x.com.samsung.networkaudio.EQband"]
async def _update_advanced_audio(self):
await self.update_execution_data(["/sec/networkaudio/advancedaudio"])
payload = await self.get_execute_status()
self.__night_mode = payload["x.com.samsung.networkaudio.nightmode"]
self.__bass_mode = payload["x.com.samsung.networkaudio.bassboost"]
self.__voice_amplifier = payload["x.com.samsung.networkaudio.voiceamplifier"]
@property
def status(self):
return self.device.status
# ------------ DEVICE INFORMATION ----------
@property
def manufacturer(self):
return self.device.status.ocf_manufacturer_name
@property
def model(self):
return self.device.status.ocf_model_number
@property
def firmware_version(self):
return self.device.status.ocf_firmware_version
@property
def device_id(self):
return self.device.device_id
@property
def device_name(self):
return self.__device_name
# ------------ ON / OFF ------------
@property
def state(self) -> str:
return "on" if self.device.status.switch else "off"
async def switch_off(self):
await self.device.switch_off(True)
async def switch_on(self):
await self.device.switch_on(True)
# ------------ VOLUME --------------
@property
def volume_level(self) -> float:
return ((self.device.status.volume / 100) * self.__max_volume) / 100
@property
def volume_muted(self) -> bool:
return self.device.status.mute
async def set_volume(self, volume: float):
"""
Sets the volume to a certain level.
This respects the max volume and hovers between
:param volume: between 0 and 1
"""
await self.device.set_volume(int(volume * self.__max_volume))
async def mute_volume(self, mute: bool):
if mute:
await self.device.unmute(True)
else:
await self.device.mute(True)
async def volume_up(self):
await self.device.volume_up(True)
async def volume_down(self):
await self.device.volume_down(True)
# ------------ WOOFER LEVEL -------------
@property
def woofer_level(self) -> int:
return self.__woofer_level
@property
def woofer_connection(self) -> str:
return self.__woofer_connection
async def set_woofer(self, level: int):
await self.set_custom_execution_data(
href="/sec/networkaudio/woofer",
property="x.com.samsung.networkaudio.woofer",
value=level,
)
# ------------ INPUT SOURCE -------------
@property
def input_source(self):
return self.device.status.input_source
@property
def supported_input_sources(self):
return self.device.status.supported_input_sources
async def select_source(self, source: str):
await self.device.set_input_source(source, True)
# ------------- SOUND MODE --------------
@property
def sound_mode(self):
return self.__active_soundmode
@property
def supported_soundmodes(self):
return self.__supported_soundmodes
async def select_sound_mode(self, sound_mode: str):
await self.set_custom_execution_data(
href="/sec/networkaudio/soundmode",
property="x.com.samsung.networkaudio.soundmode",
value=sound_mode,
)
# ------------- ADVANCED AUDIO ---------------
@property
def night_mode(self) -> bool:
return True if self.__night_mode == 1 else False
async def set_night_mode(self, value: bool):
await self.set_custom_execution_data(
href="/sec/networkaudio/advancedaudio",
property="x.com.samsung.networkaudio.nightmode",
value=1 if value else 0,
)
self.__night_mode = 1 if value else 0
@property
def bass_mode(self) -> bool:
return True if self.__bass_mode == 1 else False
async def set_bass_mode(self, value: bool):
await self.set_custom_execution_data(
href="/sec/networkaudio/advancedaudio",
property="x.com.samsung.networkaudio.bassboost",
value=1 if value else 0,
)
self.__bass_mode = 1 if value else 0
@property
def voice_amplifier(self) -> bool:
return True if self.__voice_amplifier == 1 else False
async def set_voice_amplifier(self, value: bool):
await self.set_custom_execution_data(
href="/sec/networkaudio/advancedaudio",
property="x.com.samsung.networkaudio.voiceamplifier",
value=1 if value else 0,
)
self.__voice_amplifier = 1 if value else 0
# ------------ EQUALIZER --------------
@property
def active_equalizer_preset(self):
return self.__active_eq_preset
@property
def supported_equalizer_presets(self):
return self.__supported_eq_presets
@property
def equalizer_action(self):
return self.__eq_action
@property
def equalizer_bands(self):
return self.__eq_bands
async def set_equalizer_preset(self, preset: str):
await self.set_custom_execution_data(
href="/sec/networkaudio/eq",
property="x.com.samsung.networkaudio.EQname",
value=preset,
)
# ------------- MEDIA ----------------
@property
def media_title(self):
return self.__media_title
@property
def media_artist(self):
return self.__media_artist
@property
def media_coverart_url(self):
return self.__media_cover_url
@property
def media_duration(self) -> int | None:
return self.device.status.attributes.get("totalTime").value
@property
def media_position(self) -> int | None:
return self.device.status.attributes.get("elapsedTime").value
async def media_play(self):
await self.device.play(True)
async def media_pause(self):
await self.device.pause(True)
async def media_stop(self):
await self.device.stop(True)
@property
def media_app_name(self):
detail_status = self.device.status.attributes.get("detailName", None)
if detail_status is not None:
return detail_status.value
return None
# ------------ SUPPORT FUNCTIONS ------------
async def update_execution_data(self, argument: str):
return await self.device.command("main", "execute", "execute", argument)
async def set_custom_execution_data(self, href: str, property: str, value):
argument = [href, {property: value}]
await self.device.command("main", "execute", "execute", argument)
async def get_execute_status(self):
url = f"https://api.smartthings.com/v1/devices/{self._device_id}/components/main/capabilities/execute/status"
request_headers = {"Authorization": "Bearer " + self._api_key}
resp = await self.__session.get(url, headers=request_headers)
dict = await resp.json()
return dict["data"]["value"]["payload"]
async def get_song_title_artwork(self, artist: str, title: str) -> str:
"""
This function loads a Music Art Cover from iTunes based on
the title and the artist
:param artist: string
:param title: string
:return: url as string
"""
query_term = f"{artist} {title}"
url = "https://itunes.apple.com/search?term=%s&media=music&entity=%s" % (
quote(query_term),
"musicTrack",
)
resp = await self.__session.get(url)
resp_dict = json.loads(await resp.text())
if len(resp_dict["results"]) != 0:
return resp_dict["results"][0]["artworkUrl100"]
@property
def retrieve_data(self):
return {
"status": self.state,
"device_information": {
"model": self.model,
"manufacture": self.manufacturer,
"firmware_version": self.firmware_version,
"device_id": self.device_id,
},
"volume": {"level": self.volume_level, "muted": self.volume_muted},
"woofer": {
"level": self.woofer_level,
"connection": self.woofer_connection,
},
"source": {
"active_source": self.input_source,
"supported_sources": self.supported_input_sources,
},
"sound_mode": {
"active_sound_mode": self.sound_mode,
"supported_sound_modes": self.supported_soundmodes,
},
"advanced_audio": {
"night_mode": self.night_mode,
"bass_mode": self.bass_mode,
"voice_amplifier": self.voice_amplifier,
},
"equalizer": {
"active_preset": self.active_equalizer_preset,
"supported_presets": self.supported_equalizer_presets,
"action": self.equalizer_action,
"bands": self.equalizer_bands,
},
"media": {
"media_title": self.media_title,
"media_artist": self.media_artist,
"media_cover_url": self.media_coverart_url,
"media_duration": self.media_duration,
"media_position": self.media_position,
},
}

View File

@ -0,0 +1,59 @@
import logging
import pysmartthings
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from pysmartthings import APIResponseError
from .const import (
CONF_ENTRY_API_KEY,
CONF_ENTRY_DEVICE_ID,
CONF_ENTRY_DEVICE_NAME,
CONF_ENTRY_MAX_VOLUME,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
async def validate_input(api, device_id: str):
try:
return await api.device(device_id)
except APIResponseError as excp:
_LOGGER.error("[Samsung Soundbar] ERROR: %s", str(excp))
raise ValueError
class ExampleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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:
_LOGGER.error(f"User Input is not filled")
try:
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(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_ENTRY_API_KEY): str,
vol.Required(CONF_ENTRY_DEVICE_ID): str,
vol.Required(CONF_ENTRY_DEVICE_NAME): str,
vol.Required(CONF_ENTRY_MAX_VOLUME): int,
}
),
)

View File

@ -0,0 +1,21 @@
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
DOMAIN = "samsung_soundbar"
CONF_CLOUD_INTEGRATION = "cloud_integration"
CONF_ENTRY_API_KEY = "api_key"
CONF_ENTRY_DEVICE_ID = "device_id"
CONF_ENTRY_DEVICE_NAME = "device_name"
CONF_ENTRY_MAX_VOLUME = "device_volume"
DEFAULT_NAME = DOMAIN
BUTTON = BUTTON_DOMAIN
SWITCH = SWITCH_DOMAIN
MEDIA_PLAYER = MEDIA_PLAYER_DOMAIN
SELECT = SELECT_DOMAIN
SUPPORTED_DOMAINS = ["media_player", "switch"]
PLATFORMS = [SWITCH, MEDIA_PLAYER, SELECT, BUTTON]

View File

@ -0,0 +1,50 @@
import logging
from homeassistant.components.image import ImageEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from .models import DeviceConfig
from .api_extension.SoundbarDevice import SoundbarDevice
from .const import DOMAIN, CONF_ENTRY_DEVICE_ID
_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(SoundbarImageEntity(device, "Image URL", hass))
async_add_entities(entities)
return True
class SoundbarImageEntity(ImageEntity):
def __init__(
self, device: SoundbarDevice, append_unique_id: str, hass: HomeAssistant
):
super().__init__(hass)
self.entity_id = f"image.{device.device_name}_{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._attr_image_url = self.__device.media_coverart_url
# ---------- GENERAL ---------------
@property
def name(self):
return self.__device.device_name

View File

@ -0,0 +1,12 @@
{
"domain": "samsung_soundbar",
"name": "Samsung Soundbar",
"version": "0.1.0",
"codeowners": ['@samuelspagl'],
"dependencies": ['pysmartthings'],
"documentation": "https://www.example.com",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": [],
"config_flow": true
}

View File

@ -0,0 +1,191 @@
import logging
from homeassistant.components.media_player import (
DEVICE_CLASS_SPEAKER,
MediaPlayerEntity,
)
from homeassistant.components.media_player.const import (
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.entity import DeviceInfo, generate_entity_id
from .models import DeviceConfig
from .api_extension.SoundbarDevice import SoundbarDevice
from .const import (
CONF_ENTRY_API_KEY,
CONF_ENTRY_DEVICE_ID,
CONF_ENTRY_DEVICE_NAME,
CONF_ENTRY_MAX_VOLUME,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "SmartThings Soundbar"
CONF_MAX_VOLUME = "max_volume"
SUPPORT_SMARTTHINGS_SOUNDBAR = (
SUPPORT_PAUSE
| SUPPORT_VOLUME_STEP
| SUPPORT_VOLUME_MUTE
| SUPPORT_VOLUME_SET
| SUPPORT_SELECT_SOURCE
| SUPPORT_TURN_OFF
| SUPPORT_TURN_ON
| SUPPORT_PLAY
| SUPPORT_STOP
| SUPPORT_SELECT_SOUND_MODE
)
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]
session = async_get_clientsession(hass)
device = device_config.device
if device.device_id == config_entry.data.get(CONF_ENTRY_DEVICE_ID):
entity_id = generate_entity_id(
"media_player.{}", device.device_name, hass=hass
)
entities.append(SmartThingsSoundbarMediaPlayer(device, entity_id, session))
async_add_entities(entities)
return True
class SmartThingsSoundbarMediaPlayer(MediaPlayerEntity):
def __init__(self, device: SoundbarDevice, entity_id: str, session):
self.session = session
self.device = device
self.entity_id = entity_id
self._attr_unique_id = f"{self.device.device_id}_mp"
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,
)
async def async_update(self):
await self.device.update()
# ---------- GENERAL SETTINGS ------------
@property
def device_class(self):
return DEVICE_CLASS_SPEAKER
@property
def supported_features(self):
return SUPPORT_SMARTTHINGS_SOUNDBAR
@property
def name(self):
return self.device.device_name
# ---------- POWER ON/OFF ------------
@property
def state(self):
return self.device.state
async def async_turn_off(self):
await self.device.switch_off()
async def async_turn_on(self):
await self.device.switch_on()
# ---------- VOLUME ------------
@property
def volume_level(self):
return self.device.volume_level
@property
def is_volume_muted(self):
return self.device.volume_muted
async def async_set_volume_level(self, volume):
await self.device.set_volume(volume)
async def async_mute_volume(self, mute):
await self.device.mute_volume(mute)
async def async_volume_up(self):
await self.device.volume_up()
async def async_volume_down(self):
await self.device.volume_down()
# ---------- INPUT SOURCES ------------
@property
def source(self):
return self.device.input_source
@property
def source_list(self):
return self.device.supported_input_sources
async def async_select_source(self, source):
await self.device.select_source(source)
# ---------- SOUND MODE ------------
@property
def sound_mode(self) -> str | None:
return self.device.sound_mode
@property
def sound_mode_list(self) -> list[str] | None:
return self.device.supported_soundmodes
async def async_select_sound_mode(self, sound_mode):
await self.device.select_sound_mode(sound_mode)
# ---------- MEDIA ------------
@property
def media_title(self):
return self.device.media_title
@property
def media_artist(self) -> str | None:
return self.device.media_artist
@property
def media_duration(self) -> int | None:
return self.device.media_duration
@property
def media_position(self):
return self.device.media_position
@property
def media_image_url(self) -> str | None:
return self.device.media_coverart_url
@property
def app_name(self) -> str | None:
return self.device.media_app_name
async def async_media_play(self):
await self.device.media_play()
async def async_media_pause(self):
await self.device.media_pause()
async def async_media_stop(self):
await self.device.media_stop()

View File

@ -0,0 +1,17 @@
from dataclasses import dataclass
from pysmartthings import SmartThings
from .api_extension.SoundbarDevice import SoundbarDevice
@dataclass
class DeviceConfig:
config: dict
device: SoundbarDevice
@dataclass
class SoundbarConfig:
api: SmartThings
devices: dict

View File

@ -0,0 +1,77 @@
import logging
from homeassistant.components.number import NumberEntity
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(
SoundbarNumberEntity(
device,
"woofer_level",
device.woofer_level,
device.set_woofer,
(-6, 12),
)
)
async_add_entities(entities)
return True
class SoundbarNumberEntity(NumberEntity):
def __init__(
self,
device: SoundbarDevice,
append_unique_id: str,
state_function,
on_function,
min_max: tuple,
):
self.entity_id = f"number.{device.device_name}_{append_unique_id}"
self.__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.__current_value_function = state_function
self.__set_value_function = on_function
self.__min_value = min_max[0]
self.__max_value = min_max[1]
# ---------- GENERAL ---------------
@property
def name(self):
return self.__device.device_name
# ------ STATE FUNCTIONS --------
@property
def native_value(self) -> float | None:
return self.__current_value_function
async def async_set_native_value(self, value: float):
if value > self.__max_value:
value = self.__min_value
if value < self.__min_value:
value = self.__min_value
await self.__set_value_function(value)

View File

@ -0,0 +1,99 @@
import logging
from homeassistant.components.switch import SwitchEntity
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(
SoundbarSwitchAdvancedAudio(
device,
"nightmode",
lambda: device.night_mode,
device.set_night_mode,
device.set_night_mode,
)
)
entities.append(
SoundbarSwitchAdvancedAudio(
device,
"bassmode",
lambda: device.bass_mode,
device.set_bass_mode,
device.set_bass_mode,
)
)
entities.append(
SoundbarSwitchAdvancedAudio(
device,
"voice_amplifier",
lambda: device.voice_amplifier,
device.set_voice_amplifier,
device.set_voice_amplifier,
)
)
async_add_entities(entities)
return True
class SoundbarSwitchAdvancedAudio(SwitchEntity):
def __init__(
self,
device: SoundbarDevice,
append_unique_id: str,
state_function,
on_function,
off_function,
):
self.entity_id = f"switch.{device.device_name}_{append_unique_id}"
self.__device = device
self._name = f"{self.__device.device_name} {append_unique_id}"
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.__state_function = state_function
self.__state = False
self.__on_function = on_function
self.__off_function = off_function
# ---------- GENERAL ---------------
@property
def name(self):
return self._name
def update(self):
self.__state = self.__state_function()
# ------ STATE FUNCTIONS --------
@property
def state(self):
return "on" if self.__state else "off"
async def async_turn_off(self):
await self.__off_function(False)
self.__state = "off"
async def async_turn_on(self):
await self.__on_function(True)
self.__state = "on"

View File

@ -0,0 +1,16 @@
{
"config":{
"step":{
"user":{
"data": {
"api_key": "SmartThings API Token",
"device_id": "Device ID",
"device_name":"Device Name",
"device_volume": "Max Volume (int)"
},
"description": "Bitte gib deine Daten ein.",
"title": "Authentifizierung"
}
}
}
}

View File

@ -0,0 +1,16 @@
{
"config":{
"step":{
"user":{
"data": {
"api_key": "SmartThings API Token",
"device_id": "Device ID",
"device_name":"Device Name",
"device_volume": "Max Volume (int)"
},
"description": "Please enter your credentials.",
"title": "Authentication"
}
}
}
}

6
hacs.json Normal file
View File

@ -0,0 +1,6 @@
{
"name": "Samsung Soundbar",
"filename": "samsung_soundbar.zip",
"render_readme": true,
"zip_release": true
}