347 lines
11 KiB
Python
347 lines
11 KiB
Python
"""Support for Songpal-enabled (Sony) media devices."""
|
|
import asyncio
|
|
import logging
|
|
from collections import OrderedDict
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
|
|
from homeassistant.components.media_player.const import (
|
|
DOMAIN,
|
|
SUPPORT_SELECT_SOURCE,
|
|
SUPPORT_TURN_OFF,
|
|
SUPPORT_TURN_ON,
|
|
SUPPORT_VOLUME_MUTE,
|
|
SUPPORT_VOLUME_SET,
|
|
SUPPORT_VOLUME_STEP,
|
|
)
|
|
from homeassistant.const import (
|
|
ATTR_ENTITY_ID,
|
|
CONF_NAME,
|
|
STATE_OFF,
|
|
STATE_ON,
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
)
|
|
from homeassistant.exceptions import PlatformNotReady
|
|
import homeassistant.helpers.config_validation as cv
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CONF_ENDPOINT = "endpoint"
|
|
|
|
PARAM_NAME = "name"
|
|
PARAM_VALUE = "value"
|
|
|
|
PLATFORM = "songpal"
|
|
|
|
SET_SOUND_SETTING = "songpal_set_sound_setting"
|
|
|
|
SUPPORT_SONGPAL = (
|
|
SUPPORT_VOLUME_SET
|
|
| SUPPORT_VOLUME_STEP
|
|
| SUPPORT_VOLUME_MUTE
|
|
| SUPPORT_SELECT_SOURCE
|
|
| SUPPORT_TURN_ON
|
|
| SUPPORT_TURN_OFF
|
|
)
|
|
|
|
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_entities, 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, poll=False)
|
|
|
|
if endpoint in hass.data[PLATFORM]:
|
|
_LOGGER.debug("The endpoint exists already, skipping setup.")
|
|
return
|
|
|
|
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_entities([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, poll=False):
|
|
"""Init."""
|
|
from songpal import Device
|
|
|
|
self._name = name
|
|
self._endpoint = endpoint
|
|
self._poll = poll
|
|
self.dev = Device(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._active_source = None
|
|
self._sources = {}
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""Return True if the device should be polled."""
|
|
return self._poll
|
|
|
|
async def initialize(self):
|
|
"""Initialize the device."""
|
|
await self.dev.get_supported_methods()
|
|
self._sysinfo = await self.dev.get_system_info()
|
|
|
|
async def async_activate_websocket(self):
|
|
"""Activate websocket for listening if wanted."""
|
|
_LOGGER.info("Activating websocket connection..")
|
|
from songpal import VolumeChange, ContentChange, PowerChange, ConnectChange
|
|
|
|
async def _volume_changed(volume: VolumeChange):
|
|
_LOGGER.debug("Volume changed: %s", volume)
|
|
self._volume = volume.volume
|
|
self._is_muted = volume.mute
|
|
await self.async_update_ha_state()
|
|
|
|
async def _source_changed(content: ContentChange):
|
|
_LOGGER.debug("Source changed: %s", content)
|
|
if content.is_input:
|
|
self._active_source = self._sources[content.source]
|
|
_LOGGER.debug("New active source: %s", self._active_source)
|
|
await self.async_update_ha_state()
|
|
else:
|
|
_LOGGER.debug("Got non-handled content change: %s", content)
|
|
|
|
async def _power_changed(power: PowerChange):
|
|
_LOGGER.debug("Power changed: %s", power)
|
|
self._state = power.status
|
|
await self.async_update_ha_state()
|
|
|
|
async def _try_reconnect(connect: ConnectChange):
|
|
_LOGGER.error(
|
|
"Got disconnected with %s, trying to reconnect.", connect.exception
|
|
)
|
|
self._available = False
|
|
self.dev.clear_notification_callbacks()
|
|
await self.async_update_ha_state()
|
|
|
|
# Try to reconnect forever, a successful reconnect will initialize
|
|
# the websocket connection again.
|
|
delay = 10
|
|
while not self._available:
|
|
_LOGGER.debug("Trying to reconnect in %s seconds", delay)
|
|
await asyncio.sleep(delay)
|
|
# We need to inform HA about the state in case we are coming
|
|
# back from a disconnected state.
|
|
await self.async_update_ha_state(force_refresh=True)
|
|
delay = min(2 * delay, 300)
|
|
|
|
_LOGGER.info("Reconnected to %s", self.name)
|
|
|
|
self.dev.on_notification(VolumeChange, _volume_changed)
|
|
self.dev.on_notification(ContentChange, _source_changed)
|
|
self.dev.on_notification(PowerChange, _power_changed)
|
|
self.dev.on_notification(ConnectChange, _try_reconnect)
|
|
|
|
async def listen_events():
|
|
await self.dev.listen_notifications()
|
|
|
|
async def handle_stop(event):
|
|
await self.dev.stop_listen_notifications()
|
|
|
|
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop)
|
|
|
|
self.hass.loop.create_task(listen_events())
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return name of the device."""
|
|
return self._name
|
|
|
|
@property
|
|
def unique_id(self):
|
|
"""Return a 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.debug("Got %s volume controls, using the first one", volumes)
|
|
|
|
volume = volumes[0]
|
|
_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 = OrderedDict()
|
|
for input_ in inputs:
|
|
self._sources[input_.uri] = input_
|
|
if input_.active:
|
|
self._active_source = input_
|
|
|
|
_LOGGER.debug("Active source: %s", self._active_source)
|
|
|
|
self._available = True
|
|
|
|
# activate notifications if wanted
|
|
if not self._poll:
|
|
await self.hass.async_create_task(self.async_activate_websocket())
|
|
except SongpalException as ex:
|
|
_LOGGER.error("Unable to update: %s", ex)
|
|
self._available = False
|
|
|
|
async def async_select_source(self, source):
|
|
"""Select source."""
|
|
for out in self._sources.values():
|
|
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 [src.title for src in self._sources.values()]
|
|
|
|
@property
|
|
def state(self):
|
|
"""Return current state."""
|
|
if self._state:
|
|
return STATE_ON
|
|
return STATE_OFF
|
|
|
|
@property
|
|
def source(self):
|
|
"""Return currently active source."""
|
|
# Avoid a KeyError when _active_source is not (yet) populated
|
|
return getattr(self._active_source, "title", 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
|