Skip to content
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

Merged
merged 11 commits into from
Nov 9, 2017
20 changes: 6 additions & 14 deletions homeassistant/components/remote/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@
vol.Optional(ATTR_DEVICE): cv.string,
vol.Optional(
ATTR_NUM_REPEATS, default=DEFAULT_NUM_REPEATS): cv.positive_int,
vol.Optional(
ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): vol.Coerce(float)
ATTR_DELAY_SECS: vol.Coerce(float),
Copy link
Member

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.

Copy link
Contributor Author

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!

})


Expand Down Expand Up @@ -141,25 +140,18 @@ def async_setup(hass, config):
def async_handle_remote_service(service):
"""Handle calls to the remote services."""
target_remotes = component.async_extract_from_service(service)

activity_id = service.data.get(ATTR_ACTIVITY)
device = service.data.get(ATTR_DEVICE)
command = service.data.get(ATTR_COMMAND)
num_repeats = service.data.get(ATTR_NUM_REPEATS)
delay_secs = service.data.get(ATTR_DELAY_SECS)
kwargs = service.data.copy()

update_tasks = []
for remote in target_remotes:
if service.service == SERVICE_TURN_ON:
yield from remote.async_turn_on(activity=activity_id)
yield from remote.async_turn_on(**kwargs)
elif service.service == SERVICE_TOGGLE:
yield from remote.async_toggle(activity=activity_id)
yield from remote.async_toggle(**kwargs)
elif service.service == SERVICE_SEND_COMMAND:
yield from remote.async_send_command(
device=device, command=command,
num_repeats=num_repeats, delay_secs=delay_secs)
yield from remote.async_send_command(**kwargs)
else:
yield from remote.async_turn_off(activity=activity_id)
yield from remote.async_turn_off(**kwargs)

if not remote.should_poll:
continue
Expand Down
121 changes: 61 additions & 60 deletions homeassistant/components/remote/harmony.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Choose a reason for hiding this comment

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

line too long (87 > 79 characters)


_LOGGER = logging.getLogger(__name__)

Expand All @@ -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({
Expand All @@ -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

Expand All @@ -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 = (
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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))

Choose a reason for hiding this comment

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

continuation line under-indented for visual indent

Choose a reason for hiding this comment

The 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."""
Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 send_command calls and b) to allow short delays like 0.4 seconds. Can I meet these goals when scheduling a call?

(This is not a regression as the existing call through pyharmony also ends up doing a sleep.)

Copy link
Member

@MartinHjelmare MartinHjelmare Oct 31, 2017

Choose a reason for hiding this comment

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

This is not a regression as the existing call through pyharmony also ends up doing a sleep.

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
6 changes: 3 additions & 3 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,9 @@ http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b89974819
# homeassistant.components.sensor.dht
# https://github.com/adafruit/Adafruit_Python_DHT/archive/da8cddf7fb629c1ef4f046ca44f42523c9cf2d11.zip#Adafruit_DHT==1.3.2

# homeassistant.components.remote.harmony
https://github.com/amelchio/pyharmony/archive/4bf627ef462c8e9a5d50b65e7523b54ed76e2b2a.zip#pyharmony==1.0.17alpha1

# homeassistant.components.media_player.braviatv
https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7

Expand Down Expand Up @@ -652,9 +655,6 @@ pyflexit==0.3
# homeassistant.components.ifttt
pyfttt==0.3

# homeassistant.components.remote.harmony
pyharmony==1.0.16

# homeassistant.components.binary_sensor.hikvision
pyhik==0.1.4

Expand Down