🚀: Initial Commit
This commit is contained in:
parent
d96323fd81
commit
0f43da8f1e
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
|
@ -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/
|
|
@ -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"
|
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
),
|
||||
)
|
|
@ -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]
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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"
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue