-
-
Notifications
You must be signed in to change notification settings - Fork 32k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove lag from Harmony remote platform #10218
Changes from 6 commits
88fc9fa
f48065c
89399c5
955818a
f818259
2a5d450
3432e59
d636ac6
c176b84
6bc6dad
ab7339d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,22 +5,24 @@ | |
https://home-assistant.io/components/remote.harmony/ | ||
""" | ||
import logging | ||
import asyncio | ||
from os import path | ||
import urllib.parse | ||
import time | ||
|
||
import voluptuous as vol | ||
|
||
import homeassistant.components.remote as remote | ||
import homeassistant.helpers.config_validation as cv | ||
from homeassistant.const import ( | ||
CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID) | ||
CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP) | ||
from homeassistant.components.remote import ( | ||
PLATFORM_SCHEMA, DOMAIN, ATTR_DEVICE, ATTR_ACTIVITY, ATTR_NUM_REPEATS, | ||
ATTR_DELAY_SECS) | ||
ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) | ||
from homeassistant.util import slugify | ||
from homeassistant.config import load_yaml_config_file | ||
|
||
REQUIREMENTS = ['pyharmony==1.0.16'] | ||
REQUIREMENTS = ['https://github.com/amelchio/pyharmony/archive/' | ||
'4bf627ef462c8e9a5d50b65e7523b54ed76e2b2a.zip#pyharmony==1.0.17alpha1'] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. line too long (87 > 79 characters) |
||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
@@ -35,6 +37,8 @@ | |
vol.Optional(CONF_HOST): cv.string, | ||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, | ||
vol.Required(ATTR_ACTIVITY, default=None): cv.string, | ||
vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): | ||
vol.Coerce(float), | ||
}) | ||
|
||
HARMONY_SYNC_SCHEMA = vol.Schema({ | ||
|
@@ -44,8 +48,6 @@ | |
|
||
def setup_platform(hass, config, add_devices, discovery_info=None): | ||
"""Set up the Harmony platform.""" | ||
import pyharmony | ||
|
||
host = None | ||
activity = None | ||
|
||
|
@@ -61,6 +63,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): | |
port = DEFAULT_PORT | ||
if override: | ||
activity = override.get(ATTR_ACTIVITY) | ||
delay_secs = override.get(ATTR_DELAY_SECS) | ||
port = override.get(CONF_PORT, DEFAULT_PORT) | ||
|
||
host = ( | ||
|
@@ -79,33 +82,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): | |
config.get(CONF_PORT), | ||
) | ||
activity = config.get(ATTR_ACTIVITY) | ||
delay_secs = config.get(ATTR_DELAY_SECS) | ||
else: | ||
hass.data[CONF_DEVICE_CACHE].append(config) | ||
return | ||
|
||
name, address, port = host | ||
_LOGGER.info("Loading Harmony Platform: %s at %s:%s, startup activity: %s", | ||
name, address, port, activity) | ||
try: | ||
_LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s", | ||
address, port) | ||
token = urllib.parse.quote_plus(pyharmony.ha_get_token(address, port)) | ||
_LOGGER.debug("Received token: %s", token) | ||
except ValueError as err: | ||
_LOGGER.warning("%s for remote: %s", err.args[0], name) | ||
return False | ||
|
||
harmony_conf_file = hass.config.path( | ||
'{}{}{}'.format('harmony_', slugify(name), '.conf')) | ||
device = HarmonyRemote( | ||
name, address, port, | ||
activity, harmony_conf_file, token) | ||
|
||
DEVICES.append(device) | ||
|
||
add_devices([device]) | ||
register_services(hass) | ||
return True | ||
try: | ||
device = HarmonyRemote( | ||
name, address, port, activity, harmony_conf_file, delay_secs) | ||
DEVICES.append(device) | ||
add_devices([device]) | ||
register_services(hass) | ||
except ValueError as err: | ||
_LOGGER.warning("Failed to initialize remote: %s", name) | ||
|
||
|
||
def register_services(hass): | ||
|
@@ -140,7 +135,7 @@ def _sync_service(service): | |
class HarmonyRemote(remote.RemoteDevice): | ||
"""Remote representation used to control a Harmony device.""" | ||
|
||
def __init__(self, name, host, port, activity, out_path, token): | ||
def __init__(self, name, host, port, activity, out_path, delay_secs): | ||
"""Initialize HarmonyRemote class.""" | ||
import pyharmony | ||
from pathlib import Path | ||
|
@@ -152,20 +147,35 @@ def __init__(self, name, host, port, activity, out_path, token): | |
self._state = None | ||
self._current_activity = None | ||
self._default_activity = activity | ||
self._token = token | ||
self._client = pyharmony.get_client(host, port, self.new_activity) | ||
self._config_path = out_path | ||
_LOGGER.debug("Retrieving harmony config using token: %s", token) | ||
self._config = pyharmony.ha_get_config(self._token, host, port) | ||
self._config = self._client.get_config() | ||
if not Path(self._config_path).is_file(): | ||
_LOGGER.debug("Writing harmony configuration to file: %s", | ||
out_path) | ||
pyharmony.ha_write_config_file(self._config, self._config_path) | ||
self._delay_secs = delay_secs | ||
|
||
@asyncio.coroutine | ||
def async_added_to_hass(self): | ||
"""Complete the initialization.""" | ||
self.hass.bus.async_listen_once( | ||
EVENT_HOMEASSISTANT_STOP, | ||
lambda event: self._client.disconnect(wait=True)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. continuation line under-indented for visual indent There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. continuation line under-indented for visual indent |
||
|
||
# Poll for initial state | ||
self.new_activity(self._client.get_current_activity()) | ||
|
||
@property | ||
def name(self): | ||
"""Return the Harmony device's name.""" | ||
return self._name | ||
|
||
@property | ||
def should_poll(self): | ||
"""Return the fact that we should not be polled.""" | ||
return False | ||
|
||
@property | ||
def device_state_attributes(self): | ||
"""Add platform specific attributes.""" | ||
|
@@ -176,60 +186,51 @@ def is_on(self): | |
"""Return False if PowerOff is the current activity, otherwise True.""" | ||
return self._current_activity not in [None, 'PowerOff'] | ||
|
||
def update(self): | ||
"""Return current activity.""" | ||
def new_activity(self, activity_id): | ||
"""Callback for updating the current activity.""" | ||
import pyharmony | ||
name = self._name | ||
_LOGGER.debug("Polling %s for current activity", name) | ||
state = pyharmony.ha_get_current_activity( | ||
self._token, self._config, self.host, self._port) | ||
_LOGGER.debug("%s current activity reported as: %s", name, state) | ||
self._current_activity = state | ||
self._state = bool(state != 'PowerOff') | ||
activity_name = pyharmony.activity_name(self._config, activity_id) | ||
_LOGGER.debug("%s activity reported as: %s", self._name, activity_name) | ||
self._current_activity = activity_name | ||
self._state = bool(self._current_activity != 'PowerOff') | ||
self.schedule_update_ha_state() | ||
|
||
def turn_on(self, **kwargs): | ||
"""Start an activity from the Harmony device.""" | ||
import pyharmony | ||
if kwargs[ATTR_ACTIVITY]: | ||
activity = kwargs[ATTR_ACTIVITY] | ||
else: | ||
activity = self._default_activity | ||
activity = kwargs.get(ATTR_ACTIVITY, self._default_activity) | ||
|
||
if activity: | ||
pyharmony.ha_start_activity( | ||
self._token, self.host, self._port, self._config, activity) | ||
activity_id = pyharmony.activity_id(self._config, activity) | ||
self._client.start_activity(activity_id) | ||
self._state = True | ||
else: | ||
_LOGGER.error("No activity specified with turn_on service") | ||
|
||
def turn_off(self, **kwargs): | ||
"""Start the PowerOff activity.""" | ||
import pyharmony | ||
pyharmony.ha_power_off(self._token, self.host, self._port) | ||
self._client.power_off() | ||
|
||
def send_command(self, command, **kwargs): | ||
"""Send a set of commands to one device.""" | ||
import pyharmony | ||
device = kwargs.pop(ATTR_DEVICE, None) | ||
def send_command(self, commands, **kwargs): | ||
"""Send a list of commands to one device.""" | ||
device = kwargs.get(ATTR_DEVICE) | ||
if device is None: | ||
_LOGGER.error("Missing required argument: device") | ||
return | ||
params = {} | ||
num_repeats = kwargs.pop(ATTR_NUM_REPEATS, None) | ||
if num_repeats is not None: | ||
params['repeat_num'] = num_repeats | ||
delay_secs = kwargs.pop(ATTR_DELAY_SECS, None) | ||
if delay_secs is not None: | ||
params['delay_secs'] = delay_secs | ||
pyharmony.ha_send_commands( | ||
self._token, self.host, self._port, device, command, **params) | ||
|
||
num_repeats = kwargs.get(ATTR_NUM_REPEATS) | ||
delay_secs = kwargs.get(ATTR_DELAY_SECS, self._delay_secs) | ||
|
||
for _ in range(num_repeats): | ||
for command in commands: | ||
self._client.send_command(device, command) | ||
time.sleep(delay_secs) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think you should sleep occupying a thread. Probably schedule a call in the future instead. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That does sound better. I made it like this for two reasons: a) to ensure that the ordering is kept if a script has multiple (This is not a regression as the existing call through pyharmony also ends up doing a sleep.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sleeping in a separate thread that is not part of the home assistant asyncio thread pool, will not risk interfering with the performance of the thread pool. Was it done in a separate thread or in the home assistant thread pool? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already today it sleeps in a home assistant thread ("SyncWorker_N"). |
||
|
||
def sync(self): | ||
"""Sync the Harmony device with the web service.""" | ||
import pyharmony | ||
_LOGGER.debug("Syncing hub with Harmony servers") | ||
pyharmony.ha_sync(self._token, self.host, self._port) | ||
self._config = pyharmony.ha_get_config( | ||
self._token, self.host, self._port) | ||
self._client.sync() | ||
self._config = self._client.get_config() | ||
_LOGGER.debug("Writing hub config to file: %s", self._config_path) | ||
pyharmony.ha_write_config_file(self._config, self._config_path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please put this inside
vol.Optional
for concistency.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, it was only the
default
that should be removed. Fixed now, thanks!