From 1a82adb054189fd40314df970da1c6be155545d6 Mon Sep 17 00:00:00 2001 From: David McNett Date: Thu, 19 Jan 2017 13:07:01 -0600 Subject: [PATCH] New platform media_player/anthemav (#5146) * Initial commit of anthemav platform. It loads but has no purpose. * Now presents a card in the UI but the values aren't real * Mute and volume polling/setting work now * Source lists and selection works now. * Reduce debug logging verbosity * Support power on/off and skip polling for details if power is off * Add some static tables to decode numerics from telnet commands * Add stub for unsupported media_play * New style anthemav uses native asyncio structure * Add device callback for asyncio * This is ugly but it works * Simplify async setup and abstract class data retrieval * Implement commands (power on and power off for now) * Add support for scan_interval and set default to 120 seconds * Pass-through to package handlers for volume and input selection * Slight restructuring to satisfy anthemav 0.9 * Load anthemav package from pypi now that it's registered * Proper app_name from a/v info * Mispelled word * media_player/anthemav initial commit of platform requirements * Philio 3-in-1 Gen 4 zwave sensor needs the no-off-event workaround. (#5120) * Add print_config_parameter service to Z-Wave (#5121) * Add print_config_param service to z-wave * Add print_config_parameter service to z-wave * Add print_config_parameter service to z-wave * Fix typos * Fix typos * Fix typo * Conform to Python/project style requirements * Making pylint happy * Bring pip requirements in agreement with the code * Bungled previous update * Remove unnecessady SCAN_INTERVAL logic I was unawre that this is performed as part of the normal platform behavior and it's unnecessary for a platform to independently implement this logic. * Refactor code based on @armills PR requests * Re-add media_play stub to avoid traceback * Align with platform reqirements * Remove references to SCAN_INTERVAL and clean up _lookup logic * Add DEFAULT_PORT assignment * Code style changes and removal of vestigial structures * CONF_NAME handling changes to allow local override to default from device * Address PR feedback from @balloob * Remove media_play function override It's no longer necesary for the platform to implement a stub media_play function override now that the Add SUPPORT_PLAY flag #5181 issue has been resolved and merged into the dev branch. * Rename callback function to async_ for clarity * Use async routines for platform methods * Convert update callback to coroutine for conformity Underlying anthemav library now properly supports coroutine callbacks instead of normal functions. Converted the platform callback to a coroutine for conformance with async operation for the device. Special thanks to @pvizeli and @armills for their invaluable remedial Python instruction! * Further callback refinements Altered the nature of callback handling based on suggestions from @pvizeli * True not needed for local push update_ha_state * Small style fix --- .coveragerc | 1 + .../components/media_player/anthemav.py | 175 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 179 insertions(+) create mode 100644 homeassistant/components/media_player/anthemav.py diff --git a/.coveragerc b/.coveragerc index 8f67671a5a2..a33538b7e63 100644 --- a/.coveragerc +++ b/.coveragerc @@ -201,6 +201,7 @@ omit = homeassistant/components/light/yeelight.py homeassistant/components/light/zengge.py homeassistant/components/lirc.py + homeassistant/components/media_player/anthemav.py homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py new file mode 100644 index 00000000000..2707a62f7bf --- /dev/null +++ b/homeassistant/components/media_player/anthemav.py @@ -0,0 +1,175 @@ +""" +Support for Anthem Network Receivers and Processors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.anthemav/ +""" +import logging +import asyncio + +import voluptuous as vol + +from homeassistant.components.media_player import ( + PLATFORM_SCHEMA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_SELECT_SOURCE, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON, STATE_UNKNOWN, + EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['anthemav==1.1.7'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'anthemav' + +DEFAULT_PORT = 14999 + +SUPPORT_ANTHEMAV = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + }) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up our socket to the AVR.""" + import anthemav + + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + name = config.get(CONF_NAME) + device = None + + _LOGGER.info('Provisioning Anthem AVR device at %s:%d', host, port) + + def async_anthemav_update_callback(message): + """Receive notification from transport that new data exists.""" + _LOGGER.info('Received update calback from AVR: %s', message) + hass.async_add_job(device.async_update_ha_state()) + + avr = yield from anthemav.Connection.create( + host=host, port=port, loop=hass.loop, + update_callback=async_anthemav_update_callback) + + device = AnthemAVR(avr, name) + + _LOGGER.debug('dump_devicedata: '+device.dump_avrdata) + _LOGGER.debug('dump_conndata: '+avr.dump_conndata) + _LOGGER.debug('dump_rawdata: '+avr.protocol.dump_rawdata) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.avr.close) + yield from async_add_devices([device]) + + +class AnthemAVR(MediaPlayerDevice): + """Entity reading values from Anthem AVR protocol.""" + + def __init__(self, avr, name): + """"Initialize entity with transport.""" + super().__init__() + self.avr = avr + self._name = name + + def _lookup(self, propname, dval=None): + return getattr(self.avr.protocol, propname, dval) + + @property + def supported_media_commands(self): + """Return flag of media commands that are supported.""" + return SUPPORT_ANTHEMAV + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return name of device.""" + return self._name or self._lookup('model') + + @property + def state(self): + """Return state of power on/off.""" + pwrstate = self._lookup('power') + + if pwrstate is True: + return STATE_ON + elif pwrstate is False: + return STATE_OFF + else: + return STATE_UNKNOWN + + @property + def is_volume_muted(self): + """Return boolean reflecting mute state on device.""" + return self._lookup('mute', False) + + @property + def volume_level(self): + """Return volume level from 0 to 1.""" + return self._lookup('volume_as_percentage', 0.0) + + @property + def media_title(self): + """Return current input name (closest we have to media title).""" + return self._lookup('input_name', 'No Source') + + @property + def app_name(self): + """Return details about current video and audio stream.""" + return self._lookup('video_input_resolution_text', '') + ' ' \ + + self._lookup('audio_input_name', '') + + @property + def source(self): + """Return currently selected input.""" + return self._lookup('input_name', "Unknown") + + @property + def source_list(self): + """Return all active, configured inputs.""" + return self._lookup('input_list', ["Unknown"]) + + @asyncio.coroutine + def async_select_source(self, source): + """Change AVR to the designated source (by name).""" + self._update_avr('input_name', source) + + @asyncio.coroutine + def async_turn_off(self): + """Turn AVR power off.""" + self._update_avr('power', False) + + @asyncio.coroutine + def async_turn_on(self): + """Turn AVR power on.""" + self._update_avr('power', True) + + @asyncio.coroutine + def async_set_volume_level(self, volume): + """Set AVR volume (0 to 1).""" + self._update_avr('volume_as_percentage', volume) + + @asyncio.coroutine + def async_mute_volume(self, mute): + """Engage AVR mute.""" + self._update_avr('mute', mute) + + def _update_avr(self, propname, value): + """Update a property in the AVR.""" + _LOGGER.info('Sending command to AVR: set '+propname+' to '+str(value)) + setattr(self.avr.protocol, propname, value) + + @property + def dump_avrdata(self): + """Return state of avr object for debugging forensics.""" + attrs = vars(self) + return( + 'dump_avrdata: ' + + ', '.join('%s: %s' % item for item in attrs.items())) diff --git a/requirements_all.txt b/requirements_all.txt index b8fe4898d70..58339edbbaa 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,6 +40,9 @@ aiohttp_cors==0.5.0 # homeassistant.components.sensor.amcrest amcrest==1.1.0 +# homeassistant.components.media_player.anthemav +anthemav==1.1.7 + # homeassistant.components.apcupsd apcaccess==0.0.4