mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
Add Songpal ("Sony Audio Control API") platform (#12143)
* Add Songpal ("Sony Audio Control API") platform This adds support for a variety of Sony soundbars and potentially many more devices using the same API. See http://vssupport.sony.net/en_ww/device.html for list of supported devices. * add songpal requirement * update coveragerc * fix linting * add service description to yaml * add entity_id * make pylint also happy. * raise PlatformNotReady when initialization fails, bump requirement for better exception handling * use noqa instead of pylint's disable, fix newline error * use async defs and awaits * Make linter happy * Changes based on code review, fix set_sound_setting. * define set_sound_setting schema on top of the file * Move initialization back to async_setup_platform * Fix linting * Fixes based on code review * Fix coveragerc ordering * Do not print out the whole exception trace when failing to update * Do not bail out when receiving more than one volume control * Set to unavailable when no volume controls are available
This commit is contained in:
committed by
Paulus Schoutsen
parent
6ed765698a
commit
39cee987d9
@ -465,6 +465,7 @@ omit =
|
||||
homeassistant/components/media_player/russound_rio.py
|
||||
homeassistant/components/media_player/russound_rnet.py
|
||||
homeassistant/components/media_player/snapcast.py
|
||||
homeassistant/components/media_player/songpal.py
|
||||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/spotify.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
|
@ -71,6 +71,7 @@ SERVICE_HANDLERS = {
|
||||
'sabnzbd': ('sensor', 'sabnzbd'),
|
||||
'bose_soundtouch': ('media_player', 'soundtouch'),
|
||||
'bluesound': ('media_player', 'bluesound'),
|
||||
'songpal': ('media_player', 'songpal'),
|
||||
}
|
||||
|
||||
CONF_IGNORE = 'ignore'
|
||||
|
@ -364,3 +364,17 @@ bluesound_clear_sleep_timer:
|
||||
entity_id:
|
||||
description: Name(s) of entities that will have the timer cleared.
|
||||
example: 'media_player.bluesound_livingroom'
|
||||
|
||||
songpal_set_sound_setting:
|
||||
description: Change sound setting.
|
||||
|
||||
fields:
|
||||
entity_id:
|
||||
description: Target device.
|
||||
example: 'media_player.my_soundbar'
|
||||
name:
|
||||
description: Name of the setting.
|
||||
example: 'nightMode'
|
||||
value:
|
||||
description: Value to set.
|
||||
example: 'on'
|
||||
|
252
homeassistant/components/media_player/songpal.py
Normal file
252
homeassistant/components/media_player/songpal.py
Normal file
@ -0,0 +1,252 @@
|
||||
"""
|
||||
Support for Songpal-enabled (Sony) media devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.songpal/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF,
|
||||
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_TURN_ON, MediaPlayerDevice, DOMAIN)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, STATE_ON, STATE_OFF, ATTR_ENTITY_ID)
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-songpal==0.0.6']
|
||||
|
||||
SUPPORT_SONGPAL = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | \
|
||||
SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
PLATFORM = "songpal"
|
||||
|
||||
SET_SOUND_SETTING = "songpal_set_sound_setting"
|
||||
|
||||
PARAM_NAME = "name"
|
||||
PARAM_VALUE = "value"
|
||||
|
||||
CONF_ENDPOINT = "endpoint"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_ENDPOINT): cv.string,
|
||||
})
|
||||
|
||||
SET_SOUND_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(PARAM_NAME): cv.string,
|
||||
vol.Required(PARAM_VALUE): cv.string})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config,
|
||||
async_add_devices, discovery_info=None):
|
||||
"""Set up the Songpal platform."""
|
||||
from songpal import SongpalException
|
||||
if PLATFORM not in hass.data:
|
||||
hass.data[PLATFORM] = {}
|
||||
|
||||
if discovery_info is not None:
|
||||
name = discovery_info["name"]
|
||||
endpoint = discovery_info["properties"]["endpoint"]
|
||||
_LOGGER.debug("Got autodiscovered %s - endpoint: %s", name, endpoint)
|
||||
|
||||
device = SongpalDevice(name, endpoint)
|
||||
else:
|
||||
name = config.get(CONF_NAME)
|
||||
endpoint = config.get(CONF_ENDPOINT)
|
||||
device = SongpalDevice(name, endpoint)
|
||||
|
||||
try:
|
||||
await device.initialize()
|
||||
except SongpalException as ex:
|
||||
_LOGGER.error("Unable to get methods from songpal: %s", ex)
|
||||
raise PlatformNotReady
|
||||
|
||||
hass.data[PLATFORM][endpoint] = device
|
||||
|
||||
async_add_devices([device], True)
|
||||
|
||||
async def async_service_handler(service):
|
||||
"""Service handler."""
|
||||
entity_id = service.data.get("entity_id", None)
|
||||
params = {key: value for key, value in service.data.items()
|
||||
if key != ATTR_ENTITY_ID}
|
||||
|
||||
for device in hass.data[PLATFORM].values():
|
||||
if device.entity_id == entity_id or entity_id is None:
|
||||
_LOGGER.debug("Calling %s (entity: %s) with params %s",
|
||||
service, entity_id, params)
|
||||
|
||||
await device.async_set_sound_setting(params[PARAM_NAME],
|
||||
params[PARAM_VALUE])
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SET_SOUND_SETTING, async_service_handler,
|
||||
schema=SET_SOUND_SCHEMA)
|
||||
|
||||
|
||||
class SongpalDevice(MediaPlayerDevice):
|
||||
"""Class representing a Songpal device."""
|
||||
|
||||
def __init__(self, name, endpoint):
|
||||
"""Init."""
|
||||
import songpal
|
||||
self._name = name
|
||||
self.endpoint = endpoint
|
||||
self.dev = songpal.Protocol(self.endpoint)
|
||||
self._sysinfo = None
|
||||
|
||||
self._state = False
|
||||
self._available = False
|
||||
self._initialized = False
|
||||
|
||||
self._volume_control = None
|
||||
self._volume_min = 0
|
||||
self._volume_max = 1
|
||||
self._volume = 0
|
||||
self._is_muted = False
|
||||
|
||||
self._sources = []
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize the device."""
|
||||
await self.dev.get_supported_methods()
|
||||
self._sysinfo = await self.dev.get_system_info()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
return self._sysinfo.macAddr
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return availability of the device."""
|
||||
return self._available
|
||||
|
||||
async def async_set_sound_setting(self, name, value):
|
||||
"""Change a setting on the device."""
|
||||
await self.dev.set_sound_settings(name, value)
|
||||
|
||||
async def async_update(self):
|
||||
"""Fetch updates from the device."""
|
||||
from songpal import SongpalException
|
||||
try:
|
||||
volumes = await self.dev.get_volume_information()
|
||||
if not volumes:
|
||||
_LOGGER.error("Got no volume controls, bailing out")
|
||||
self._available = False
|
||||
return
|
||||
|
||||
if len(volumes) > 1:
|
||||
_LOGGER.warning("Got %s volume controls, using the first one",
|
||||
volumes)
|
||||
|
||||
volume = volumes.pop()
|
||||
_LOGGER.debug("Current volume: %s", volume)
|
||||
|
||||
self._volume_max = volume.maxVolume
|
||||
self._volume_min = volume.minVolume
|
||||
self._volume = volume.volume
|
||||
self._volume_control = volume
|
||||
self._is_muted = self._volume_control.is_muted
|
||||
|
||||
status = await self.dev.get_power()
|
||||
self._state = status.status
|
||||
_LOGGER.debug("Got state: %s", status)
|
||||
|
||||
inputs = await self.dev.get_inputs()
|
||||
_LOGGER.debug("Got ins: %s", inputs)
|
||||
self._sources = inputs
|
||||
|
||||
self._available = True
|
||||
except SongpalException as ex:
|
||||
# if we were available, print out the exception
|
||||
if self._available:
|
||||
_LOGGER.error("Got an exception: %s", ex)
|
||||
self._available = False
|
||||
|
||||
async def async_select_source(self, source):
|
||||
"""Select source."""
|
||||
for out in self._sources:
|
||||
if out.title == source:
|
||||
await out.activate()
|
||||
return
|
||||
|
||||
_LOGGER.error("Unable to find output: %s", source)
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""Return list of available sources."""
|
||||
return [x.title for x in self._sources]
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return current state."""
|
||||
if self._state:
|
||||
return STATE_ON
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Return currently active source."""
|
||||
for out in self._sources:
|
||||
if out.active:
|
||||
return out.title
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Return volume level."""
|
||||
volume = self._volume / self._volume_max
|
||||
return volume
|
||||
|
||||
async def async_set_volume_level(self, volume):
|
||||
"""Set volume level."""
|
||||
volume = int(volume * self._volume_max)
|
||||
_LOGGER.debug("Setting volume to %s", volume)
|
||||
return await self._volume_control.set_volume(volume)
|
||||
|
||||
async def async_volume_up(self):
|
||||
"""Set volume up."""
|
||||
return await self._volume_control.set_volume("+1")
|
||||
|
||||
async def async_volume_down(self):
|
||||
"""Set volume down."""
|
||||
return await self._volume_control.set_volume("-1")
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn the device on."""
|
||||
return await self.dev.set_power(True)
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn the device off."""
|
||||
return await self.dev.set_power(False)
|
||||
|
||||
async def async_mute_volume(self, mute):
|
||||
"""Mute or unmute the device."""
|
||||
_LOGGER.debug("Set mute: %s", mute)
|
||||
return await self._volume_control.set_mute(mute)
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
"""Return whether the device is muted."""
|
||||
return self._is_muted
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return supported features."""
|
||||
return SUPPORT_SONGPAL
|
@ -958,6 +958,9 @@ python-roku==3.1.5
|
||||
# homeassistant.components.sensor.sochain
|
||||
python-sochain-api==0.0.2
|
||||
|
||||
# homeassistant.components.media_player.songpal
|
||||
python-songpal==0.0.6
|
||||
|
||||
# homeassistant.components.sensor.synologydsm
|
||||
python-synology==0.1.0
|
||||
|
||||
|
Reference in New Issue
Block a user