* Remove unnecessary exception re-wraps * Preserve exception chains on re-raise We slap "from cause" to almost all possible cases here. In some cases it could conceivably be better to do "from None" if we really want to hide the cause. However those should be in the minority, and "from cause" should be an improvement over the corresponding raise without a "from" in all cases anyway. The only case where we raise from None here is in plex, where the exception for an original invalid SSL cert is not the root cause for failure to validate a newly fetched one. Follow local convention on exception variable names if there is a consistent one, otherwise `err` to match with majority of codebase. * Fix mistaken re-wrap in homematicip_cloud/hap.py Missed the difference between HmipConnectionError and HmipcConnectionError. * Do not hide original error on plex new cert validation error Original is not the cause for the new one, but showing old in the traceback is useful nevertheless.
205 lines
5.9 KiB
Python
205 lines
5.9 KiB
Python
"""Support for the Unitymedia Horizon HD Recorder."""
|
|
from datetime import timedelta
|
|
import logging
|
|
|
|
from horimote import Client, keys
|
|
from horimote.exceptions import AuthenticationError
|
|
import voluptuous as vol
|
|
|
|
from homeassistant import util
|
|
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
|
|
from homeassistant.components.media_player.const import (
|
|
MEDIA_TYPE_CHANNEL,
|
|
SUPPORT_NEXT_TRACK,
|
|
SUPPORT_PAUSE,
|
|
SUPPORT_PLAY,
|
|
SUPPORT_PLAY_MEDIA,
|
|
SUPPORT_PREVIOUS_TRACK,
|
|
SUPPORT_TURN_OFF,
|
|
SUPPORT_TURN_ON,
|
|
)
|
|
from homeassistant.const import (
|
|
CONF_HOST,
|
|
CONF_NAME,
|
|
CONF_PORT,
|
|
STATE_OFF,
|
|
STATE_PAUSED,
|
|
STATE_PLAYING,
|
|
)
|
|
from homeassistant.exceptions import PlatformNotReady
|
|
import homeassistant.helpers.config_validation as cv
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DEFAULT_NAME = "Horizon"
|
|
DEFAULT_PORT = 5900
|
|
|
|
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
|
|
|
SUPPORT_HORIZON = (
|
|
SUPPORT_NEXT_TRACK
|
|
| SUPPORT_PAUSE
|
|
| SUPPORT_PLAY
|
|
| SUPPORT_PLAY_MEDIA
|
|
| SUPPORT_PREVIOUS_TRACK
|
|
| SUPPORT_TURN_ON
|
|
| SUPPORT_TURN_OFF
|
|
)
|
|
|
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|
{
|
|
vol.Required(CONF_HOST): cv.string,
|
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
|
}
|
|
)
|
|
|
|
|
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
|
"""Set up the Horizon platform."""
|
|
|
|
host = config[CONF_HOST]
|
|
name = config[CONF_NAME]
|
|
port = config[CONF_PORT]
|
|
|
|
try:
|
|
client = Client(host, port=port)
|
|
except AuthenticationError as msg:
|
|
_LOGGER.error("Authentication to %s at %s failed: %s", name, host, msg)
|
|
return
|
|
except OSError as msg:
|
|
# occurs if horizon box is offline
|
|
_LOGGER.error("Connection to %s at %s failed: %s", name, host, msg)
|
|
raise PlatformNotReady from msg
|
|
|
|
_LOGGER.info("Connection to %s at %s established", name, host)
|
|
|
|
add_entities([HorizonDevice(client, name, keys)], True)
|
|
|
|
|
|
class HorizonDevice(MediaPlayerEntity):
|
|
"""Representation of a Horizon HD Recorder."""
|
|
|
|
def __init__(self, client, name, remote_keys):
|
|
"""Initialize the remote."""
|
|
self._client = client
|
|
self._name = name
|
|
self._state = None
|
|
self._keys = remote_keys
|
|
|
|
@property
|
|
def name(self):
|
|
"""Return the name of the remote."""
|
|
return self._name
|
|
|
|
@property
|
|
def state(self):
|
|
"""Return the state of the device."""
|
|
return self._state
|
|
|
|
@property
|
|
def supported_features(self):
|
|
"""Flag media player features that are supported."""
|
|
return SUPPORT_HORIZON
|
|
|
|
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
|
def update(self):
|
|
"""Update State using the media server running on the Horizon."""
|
|
try:
|
|
if self._client.is_powered_on():
|
|
self._state = STATE_PLAYING
|
|
else:
|
|
self._state = STATE_OFF
|
|
except OSError:
|
|
self._state = STATE_OFF
|
|
|
|
def turn_on(self):
|
|
"""Turn the device on."""
|
|
if self._state == STATE_OFF:
|
|
self._send_key(self._keys.POWER)
|
|
|
|
def turn_off(self):
|
|
"""Turn the device off."""
|
|
if self._state != STATE_OFF:
|
|
self._send_key(self._keys.POWER)
|
|
|
|
def media_previous_track(self):
|
|
"""Channel down."""
|
|
self._send_key(self._keys.CHAN_DOWN)
|
|
self._state = STATE_PLAYING
|
|
|
|
def media_next_track(self):
|
|
"""Channel up."""
|
|
self._send_key(self._keys.CHAN_UP)
|
|
self._state = STATE_PLAYING
|
|
|
|
def media_play(self):
|
|
"""Send play command."""
|
|
self._send_key(self._keys.PAUSE)
|
|
self._state = STATE_PLAYING
|
|
|
|
def media_pause(self):
|
|
"""Send pause command."""
|
|
self._send_key(self._keys.PAUSE)
|
|
self._state = STATE_PAUSED
|
|
|
|
def media_play_pause(self):
|
|
"""Send play/pause command."""
|
|
self._send_key(self._keys.PAUSE)
|
|
if self._state == STATE_PAUSED:
|
|
self._state = STATE_PLAYING
|
|
else:
|
|
self._state = STATE_PAUSED
|
|
|
|
def play_media(self, media_type, media_id, **kwargs):
|
|
"""Play media / switch to channel."""
|
|
if MEDIA_TYPE_CHANNEL == media_type:
|
|
try:
|
|
self._select_channel(int(media_id))
|
|
self._state = STATE_PLAYING
|
|
except ValueError:
|
|
_LOGGER.error("Invalid channel: %s", media_id)
|
|
else:
|
|
_LOGGER.error(
|
|
"Invalid media type %s. Supported type: %s",
|
|
media_type,
|
|
MEDIA_TYPE_CHANNEL,
|
|
)
|
|
|
|
def _select_channel(self, channel):
|
|
"""Select a channel (taken from einder library, thx)."""
|
|
self._send(channel=channel)
|
|
|
|
def _send_key(self, key):
|
|
"""Send a key to the Horizon device."""
|
|
self._send(key=key)
|
|
|
|
def _send(self, key=None, channel=None):
|
|
"""Send a key to the Horizon device."""
|
|
|
|
try:
|
|
if key:
|
|
self._client.send_key(key)
|
|
elif channel:
|
|
self._client.select_channel(channel)
|
|
except OSError as msg:
|
|
_LOGGER.error(
|
|
"%s disconnected: %s. Trying to reconnect...", self._name, msg
|
|
)
|
|
|
|
# for reconnect, first gracefully disconnect
|
|
self._client.disconnect()
|
|
|
|
try:
|
|
self._client.connect()
|
|
self._client.authorize()
|
|
except AuthenticationError as msg:
|
|
_LOGGER.error("Authentication to %s failed: %s", self._name, msg)
|
|
return
|
|
except OSError as msg:
|
|
# occurs if horizon box is offline
|
|
_LOGGER.error("Reconnect to %s failed: %s", self._name, msg)
|
|
return
|
|
|
|
self._send(key=key, channel=channel)
|