From be297c4c7e670bc1e3a9c558114f571f0bafc8da Mon Sep 17 00:00:00 2001 From: Krasimir Zhelev Date: Tue, 28 Feb 2017 15:23:07 +0100 Subject: [PATCH] Frontier silicon (#6131) * added frontier_silicon constant * added the frontier_silicon component * cleaning up according to travis * trying to satisfy pylint * trying to satisfy pylint * fsapi version 0.0.6 * with fsapi version 0.0.7 * added fsapi dependency * yielding the FSAPI * Removing white space from docstring * Removing white space from an empty line * Switching to sync * clean up white spaces and rename device to FSAPIDevice * added frontier_silicon constant * added the frontier_silicon component * cleaning up according to travis * trying to satisfy pylint * trying to satisfy pylint * fsapi version 0.0.6 * with fsapi version 0.0.7 * added fsapi dependency * yielding the FSAPI * Removing white space from docstring * Removing white space from an empty line * Switching to sync * clean up white spaces and rename device to FSAPIDevice * changed info to debug * added frontier_silicon constant * added the frontier_silicon component * cleaning up according to travis * trying to satisfy pylint * trying to satisfy pylint * fsapi version 0.0.6 * with fsapi version 0.0.7 * added fsapi dependency * yielding the FSAPI * Removing white space from docstring * Removing white space from an empty line * Switching to sync * clean up white spaces and rename device to FSAPIDevice * added the frontier_silicon component * trying to satisfy pylint * fsapi version 0.0.6 * remove white space * generated requirements * added the frontier_silicon component * cleaning up according to travis * trying to satisfy pylint * trying to satisfy pylint * fsapi version 0.0.6 * with fsapi version 0.0.7 * added fsapi dependency * yielding the FSAPI * Removing white space from docstring * Removing white space from an empty line * Switching to sync * clean up white spaces and rename device to FSAPIDevice * trying to satisfy pylint * changed info to debug * added the frontier_silicon component * fsapi version 0.0.6 * generated requirements * pylint * moved import requests to the method where it is being used * add a basic unit test * cleaned up source code * added frontier_silicon constant * added the frontier_silicon component * added basic test * added fsapi to requirements_all.txt * added coverage omit, though a basic test was included * added MEDIA_TYPE_MUSIC for artist and album * removed duplicate cons * switched fsapi call to a property, removed unecessary comment * detailed docstring for fs_device * added a space for the info_name - info_text separator * reduced proeprty (fsapi) access for volume down/up --- .coveragerc | 1 + homeassistant/components/discovery.py | 1 + .../media_player/frontier_silicon.py | 252 ++++++++++++++++++ requirements_all.txt | 3 + .../media_player/test_frontier_silicon.py | 42 +++ 5 files changed, 299 insertions(+) create mode 100644 homeassistant/components/media_player/frontier_silicon.py create mode 100644 tests/components/media_player/test_frontier_silicon.py diff --git a/.coveragerc b/.coveragerc index 4db8323c3a1..c88856c724e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -234,6 +234,7 @@ omit = homeassistant/components/media_player/dunehd.py homeassistant/components/media_player/emby.py homeassistant/components/media_player/firetv.py + homeassistant/components/media_player/frontier_silicon.py homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/gstreamer.py homeassistant/components/media_player/hdmi_cec.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index b8999ee2c43..284e8c042da 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -42,6 +42,7 @@ SERVICE_HANDLERS = { 'yeelight': ('light', 'yeelight'), 'flux_led': ('light', 'flux_led'), 'apple_tv': ('media_player', 'apple_tv'), + 'frontier_silicon': ('media_player', 'frontier_silicon'), 'openhome': ('media_player', 'openhome'), } diff --git a/homeassistant/components/media_player/frontier_silicon.py b/homeassistant/components/media_player/frontier_silicon.py new file mode 100644 index 00000000000..386a489b646 --- /dev/null +++ b/homeassistant/components/media_player/frontier_silicon.py @@ -0,0 +1,252 @@ +""" +Support for Frontier Silicon Devices (Medion, Hama, Auna,...). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.frontier_silicon/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.media_player import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_PLAY, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, + MEDIA_TYPE_MUSIC) +from homeassistant.const import ( + STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN, + CONF_HOST, CONF_PORT, CONF_PASSWORD) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['fsapi==0.0.7'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_FRONTIER_SILICON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \ + SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ + SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_ON | \ + SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + +DEFAULT_PORT = 80 +DEFAULT_PASSWORD = '1234' +DEVICE_URL = 'http://{0}:{1}/device' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Frontier Silicon platform.""" + import requests + + if discovery_info is not None: + add_devices( + [FSAPIDevice(discovery_info, DEFAULT_PASSWORD)], + update_before_add=True) + return True + + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + password = config.get(CONF_PASSWORD) + + try: + add_devices( + [FSAPIDevice(DEVICE_URL.format(host, port), password)], + update_before_add=True) + _LOGGER.debug('FSAPI device %s:%s -> %s', host, port, password) + return True + except requests.exceptions.RequestException: + _LOGGER.error('Could not add the FSAPI device at %s:%s -> %s', + host, port, password) + + return False + + +class FSAPIDevice(MediaPlayerDevice): + """Representation of a Frontier Silicon device on the network.""" + + def __init__(self, device_url, password): + """Initialize the Frontier Silicon API device.""" + self._device_url = device_url + self._password = password + self._state = STATE_UNKNOWN + + self._name = None + self._title = None + self._artist = None + self._album_name = None + self._mute = None + self._source = None + self._source_list = None + self._media_image_url = None + + # Properties + @property + def fs_device(self): + """ + Create a fresh fsapi session. + + A new session is created for each request in case someone else + connected to the device in between the updates and invalidated the + existing session (i.e UNDOK). + """ + from fsapi import FSAPI + + return FSAPI(self._device_url, self._password) + + @property + def should_poll(self): + """Device should be polled.""" + return True + + @property + def name(self): + """Return the device name.""" + return self._name + + @property + def media_title(self): + """Title of current playing media.""" + return self._title + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + return self._artist + + @property + def media_album_name(self): + """Album name of current playing media, music track only.""" + return self._album_name + + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def supported_features(self): + """Flag of media commands that are supported.""" + return SUPPORT_FRONTIER_SILICON + + @property + def state(self): + """Return the state of the player.""" + return self._state + + # source + @property + def source_list(self): + """List of available input sources.""" + return self._source_list + + @property + def source(self): + """Name of the current input source.""" + return self._source + + @property + def media_image_url(self): + """Image url of current playing media.""" + return self._media_image_url + + def update(self): + """Get the latest date and update device state.""" + fs_device = self.fs_device + + if not self._name: + self._name = fs_device.friendly_name + + if not self._source_list: + self._source_list = fs_device.mode_list + + status = fs_device.play_status + self._state = { + 'playing': STATE_PLAYING, + 'paused': STATE_PAUSED, + 'stopped': STATE_OFF, + 'unknown': STATE_UNKNOWN, + None: STATE_OFF, + }.get(status, STATE_UNKNOWN) + + info_name = fs_device.play_info_name + info_text = fs_device.play_info_text + + self._title = ' - '.join(filter(None, [info_name, info_text])) + self._artist = fs_device.play_info_artist + self._album_name = fs_device.play_info_album + + self._source = fs_device.mode + self._mute = fs_device.mute + self._media_image_url = fs_device.play_info_graphics + + # Management actions + + # power control + def turn_on(self): + """Turn on the device.""" + self.fs_device.power = True + + def turn_off(self): + """Turn off the device.""" + self.fs_device.power = False + + def media_play(self): + """Send play command.""" + self.fs_device.play() + + def media_pause(self): + """Send pause command.""" + self.fs_device.pause() + + def media_play_pause(self): + """Send play/pause command.""" + if 'playing' in self._state: + self.fs_device.pause() + else: + self.fs_device.play() + + def media_stop(self): + """Send play/pause command.""" + self.fs_device.pause() + + def media_previous_track(self): + """Send previous track command (results in rewind).""" + self.fs_device.prev() + + def media_next_track(self): + """Send next track command (results in fast-forward).""" + self.fs_device.next() + + # mute + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._mute + + def mute_volume(self, mute): + """Send mute command.""" + self.fs_device.mute = mute + + # volume + def volume_up(self): + """Send volume up command.""" + self.fs_device.volume += 1 + + def volume_down(self): + """Send volume down command.""" + self.fs_device.volume -= 1 + + def set_volume_level(self, volume): + """Set volume command.""" + self.fs_device.volume = volume + + def select_source(self, source): + """Select input source.""" + self.fs_device.mode = source diff --git a/requirements_all.txt b/requirements_all.txt index 6c883ff4f2d..9f69acee1b4 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -167,6 +167,9 @@ freesms==0.1.1 # homeassistant.components.switch.fritzdect fritzhome==1.0.2 +# homeassistant.components.media_player.frontier_silicon +fsapi==0.0.7 + # homeassistant.components.conversation fuzzywuzzy==0.15.0 diff --git a/tests/components/media_player/test_frontier_silicon.py b/tests/components/media_player/test_frontier_silicon.py new file mode 100644 index 00000000000..a2c3223cd9c --- /dev/null +++ b/tests/components/media_player/test_frontier_silicon.py @@ -0,0 +1,42 @@ +"""The tests for the Demo Media player platform.""" +import unittest +from unittest import mock + +import logging + +from homeassistant.components.media_player.frontier_silicon import FSAPIDevice +from homeassistant.components.media_player import frontier_silicon +from homeassistant import const + +from tests.common import get_test_home_assistant + +_LOGGER = logging.getLogger(__name__) + + +class TestFrontierSiliconMediaPlayer(unittest.TestCase): + """Test the media_player module.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + def test_host_required_with_host(self): + """Test that a host with a valid url is set when using a conf.""" + fake_config = { + const.CONF_HOST: 'host_ip', + } + result = frontier_silicon.setup_platform(self.hass, + fake_config, mock.MagicMock()) + + self.assertTrue(result) + + def test_invalid_host(self): + """Test that a host with a valid url is set when using a conf.""" + import requests + + fsapi = FSAPIDevice('INVALID_URL', '1234') + self.assertRaises(requests.exceptions.MissingSchema, fsapi.update)