Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 99 additions & 19 deletions homeassistant/components/media_player/songpal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.songpal/
"""
import asyncio
import logging
from collections import OrderedDict

import voluptuous as vol

from homeassistant.components.media_player import (
DOMAIN, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP, MediaPlayerDevice)
from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON
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

REQUIREMENTS = ['python-songpal==0.0.8']
REQUIREMENTS = ['python-songpal==0.0.9.1']

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -62,7 +65,11 @@ async def async_setup_platform(
else:
name = config.get(CONF_NAME)
endpoint = config.get(CONF_ENDPOINT)
device = SongpalDevice(name, 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()
Expand Down Expand Up @@ -96,12 +103,13 @@ async def async_service_handler(service):
class SongpalDevice(MediaPlayerDevice):
"""Class representing a Songpal device."""

def __init__(self, name, endpoint):
def __init__(self, name, endpoint, poll=False):
"""Init."""
import songpal
from songpal import Device
self._name = name
self.endpoint = endpoint
self.dev = songpal.Device(self.endpoint)
self._endpoint = endpoint
self._poll = poll
self.dev = Device(self._endpoint)
self._sysinfo = None

self._state = False
Expand All @@ -114,13 +122,79 @@ def __init__(self, name, endpoint):
self._volume = 0
self._is_muted = False

self._sources = []
self._active_source = None
self._sources = {}

@property
def should_poll(self):
"""Return True if the device should be polled."""
return self._poll
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep the attribute?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer keeping it around for the time being, if someone comes up with a broken setup it makes it easy to check if the polling variant would work in that case.


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.warning("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)

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."""
Expand Down Expand Up @@ -169,18 +243,28 @@ async def async_update(self):

inputs = await self.dev.get_inputs()
_LOGGER.debug("Got ins: %s", inputs)
self._sources = 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:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this check?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above. If you think it's better to remove it, I'll do that.

await self.hass.async_create_task(
self.async_activate_websocket())
except SongpalException as ex:
# if we were available, print out the exception
if self._available:
_LOGGER.error("Got an exception: %s", 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:
for out in self._sources.values():
if out.title == source:
await out.activate()
return
Expand All @@ -190,7 +274,7 @@ async def async_select_source(self, source):
@property
def source_list(self):
"""Return list of available sources."""
return [x.title for x in self._sources]
return [src.title for src in self._sources.values()]

@property
def state(self):
Expand All @@ -202,11 +286,7 @@ def state(self):
@property
def source(self):
"""Return currently active source."""
for out in self._sources:
if out.active:
return out.title

return None
return self._active_source.title

@property
def volume_level(self):
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,7 @@ python-roku==3.1.5
python-sochain-api==0.0.2

# homeassistant.components.media_player.songpal
python-songpal==0.0.8
python-songpal==0.0.9.1

# homeassistant.components.sensor.synologydsm
python-synology==0.2.0
Expand Down