Use aioharmony for remote.harmony platform (#19595)

* Use aioharmony for async

Use aioharmony to interact with Harmony hub. Due to this following improvements:
-) Setting of available state for entity
-) Automatic config update if configuration changes (including updating file containing config)
-) Allow using of device name instead of number
-) When sending command with repeat, nothing else will be able to put a IR command in between

* Requirements updated

* Version update for fix

* Mainly cleanup

* Update requirements

Updated requirements

* Fixed lint issue

* Small bump for aioharmony

Small version bump increase for aioharmony

* Updated based on review
This commit is contained in:
ehendrix23 2018-12-29 18:22:27 -07:00 committed by Martin Hjelmare
parent 9aa6037219
commit faeee4f7ad
2 changed files with 163 additions and 79 deletions

View File

@ -7,32 +7,28 @@ https://home-assistant.io/components/remote.harmony/
import asyncio import asyncio
import json import json
import logging import logging
from datetime import timedelta
from pathlib import Path
import voluptuous as vol import voluptuous as vol
from homeassistant.components import remote from homeassistant.components import remote
from homeassistant.components.remote import ( from homeassistant.components.remote import (
ATTR_ACTIVITY, ATTR_DELAY_SECS, ATTR_DEVICE, ATTR_NUM_REPEATS, ATTR_ACTIVITY, ATTR_DELAY_SECS, ATTR_DEVICE, ATTR_NUM_REPEATS,
DEFAULT_DELAY_SECS, DOMAIN, PLATFORM_SCHEMA) DEFAULT_DELAY_SECS, DOMAIN, PLATFORM_SCHEMA
)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP) ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP
)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.util import slugify from homeassistant.util import slugify
# REQUIREMENTS = ['pyharmony==1.0.22'] REQUIREMENTS = ['aioharmony==0.1.1']
REQUIREMENTS = [
'https://github.com/home-assistant/pyharmony/archive/'
'31efd339a3c39e7b8f58e823a0eddb59013e03ae.zip'
'#pyharmony==1.0.21b1'
]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_CURRENT_ACTIVITY = 'current_activity'
DEFAULT_PORT = 8088 DEFAULT_PORT = 8088
SCAN_INTERVAL = timedelta(seconds=5)
DEVICES = [] DEVICES = []
CONF_DEVICE_CACHE = 'harmony_device_cache' CONF_DEVICE_CACHE = 'harmony_device_cache'
@ -55,7 +51,6 @@ HARMONY_SYNC_SCHEMA = vol.Schema({
async def async_setup_platform(hass, config, async_add_entities, async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None): discovery_info=None):
"""Set up the Harmony platform.""" """Set up the Harmony platform."""
host = None
activity = None activity = None
if CONF_DEVICE_CACHE not in hass.data: if CONF_DEVICE_CACHE not in hass.data:
@ -65,11 +60,11 @@ async def async_setup_platform(hass, config, async_add_entities,
# Find the discovered device in the list of user configurations # Find the discovered device in the list of user configurations
override = next((c for c in hass.data[CONF_DEVICE_CACHE] override = next((c for c in hass.data[CONF_DEVICE_CACHE]
if c.get(CONF_NAME) == discovery_info.get(CONF_NAME)), if c.get(CONF_NAME) == discovery_info.get(CONF_NAME)),
False) None)
port = DEFAULT_PORT port = DEFAULT_PORT
delay_secs = DEFAULT_DELAY_SECS delay_secs = DEFAULT_DELAY_SECS
if override: if override is not None:
activity = override.get(ATTR_ACTIVITY) activity = override.get(ATTR_ACTIVITY)
delay_secs = override.get(ATTR_DELAY_SECS) delay_secs = override.get(ATTR_DELAY_SECS)
port = override.get(CONF_PORT, DEFAULT_PORT) port = override.get(CONF_PORT, DEFAULT_PORT)
@ -130,7 +125,6 @@ async def _apply_service(service, service_func, *service_func_args):
for device in _devices: for device in _devices:
await service_func(device, *service_func_args) await service_func(device, *service_func_args)
device.schedule_update_ha_state(True)
async def _sync_service(service): async def _sync_service(service):
@ -142,39 +136,53 @@ class HarmonyRemote(remote.RemoteDevice):
def __init__(self, name, host, port, activity, out_path, delay_secs): def __init__(self, name, host, port, activity, out_path, delay_secs):
"""Initialize HarmonyRemote class.""" """Initialize HarmonyRemote class."""
import pyharmony.client as harmony_client from aioharmony.harmonyapi import (
HarmonyAPI as HarmonyClient, ClientCallbackType
)
_LOGGER.debug("HarmonyRemote device init started for: %s", name) _LOGGER.debug("%s: Device init started", name)
self._name = name self._name = name
self.host = host self.host = host
self.port = port self.port = port
self._state = None self._state = None
self._current_activity = None self._current_activity = None
self._default_activity = activity self._default_activity = activity
# self._client = pyharmony.get_client(host, port, self.new_activity) self._client = HarmonyClient(
self._client = harmony_client.HarmonyClient(host) ip_address=host,
callbacks=ClientCallbackType(
new_activity=self.new_activity,
config_updated=self.new_config,
connect=self.got_connected,
disconnect=self.got_disconnected
)
)
self._config_path = out_path self._config_path = out_path
self._delay_secs = delay_secs self._delay_secs = delay_secs
_LOGGER.debug("HarmonyRemote device init completed for: %s", name) self._available = False
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Complete the initialization.""" """Complete the initialization."""
_LOGGER.debug("HarmonyRemote added for: %s", self._name) _LOGGER.debug("%s: Harmony Hub added", self._name)
import aioharmony.exceptions as aioexc
async def shutdown(event): async def shutdown(_):
"""Close connection on shutdown.""" """Close connection on shutdown."""
await self._client.disconnect() _LOGGER.debug("%s: Closing Harmony Hub", self._name)
try:
await self._client.close()
except aioexc.TimeOut:
_LOGGER.warning("%s: Disconnect timed-out", self._name)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
_LOGGER.debug("Connecting.") _LOGGER.debug("%s: Connecting", self._name)
await self._client.connect() try:
await self._client.get_config() await self._client.connect()
if not Path(self._config_path).is_file(): except aioexc.TimeOut:
self.write_config_file() _LOGGER.error("%s: Connection timed-out", self._name)
else:
# Poll for initial state # Set initial state
self.new_activity(await self._client.get_current_activity()) self.new_activity(self._client.current_activity)
@property @property
def name(self): def name(self):
@ -184,113 +192,189 @@ class HarmonyRemote(remote.RemoteDevice):
@property @property
def should_poll(self): def should_poll(self):
"""Return the fact that we should not be polled.""" """Return the fact that we should not be polled."""
return True return False
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Add platform specific attributes.""" """Add platform specific attributes."""
return {'current_activity': self._current_activity} return {ATTR_CURRENT_ACTIVITY: self._current_activity}
@property @property
def is_on(self): def is_on(self):
"""Return False if PowerOff is the current activity, otherwise True.""" """Return False if PowerOff is the current activity, otherwise True."""
return self._current_activity not in [None, 'PowerOff'] return self._current_activity not in [None, 'PowerOff']
async def async_update(self): @property
"""Retrieve current activity from Hub.""" def available(self):
_LOGGER.debug("Updating Harmony.") """Return True if connected to Hub, otherwise False."""
if not self._client.config: return self._available
await self._client.get_config()
activity_id = await self._client.get_current_activity() def new_activity(self, activity_info: tuple) -> None:
activity_name = self._client.get_activity_name(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')
return
def new_activity(self, activity_id):
"""Call for updating the current activity.""" """Call for updating the current activity."""
activity_name = self._client.get_activity_name(activity_id) activity_id, activity_name = activity_info
_LOGGER.debug("%s activity reported as: %s", self._name, activity_name) _LOGGER.debug("%s: activity reported as: %s", self._name,
activity_name)
self._current_activity = activity_name self._current_activity = activity_name
self._state = bool(self._current_activity != 'PowerOff') self._state = bool(activity_id != -1)
self.schedule_update_ha_state() self.async_schedule_update_ha_state()
async def new_config(self, _=None):
"""Call for updating the current activity."""
_LOGGER.debug("%s: configuration has been updated", self._name)
self.new_activity(self._client.current_activity)
await self.hass.async_add_executor_job(self.write_config_file)
def got_connected(self, _=None):
"""Notification that we're connected to the HUB."""
_LOGGER.debug("%s: connected to the HUB.", self._name)
if not self._available:
# We were disconnected before.
self.new_config()
self._available = True
async def got_disconnected(self, _=None):
"""Notification that we're disconnected from the HUB."""
_LOGGER.debug("%s: disconnected from the HUB.", self._name)
self._available = False
# We're going to wait for 10 seconds before announcing we're
# unavailable, this to allow a reconnection to happen.
await asyncio.sleep(10)
if not self._available:
# Still disconnected. Let the state engine know.
self.async_schedule_update_ha_state()
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Start an activity from the Harmony device.""" """Start an activity from the Harmony device."""
import aioharmony.exceptions as aioexc
_LOGGER.debug("%s: Turn On", self.name)
activity = kwargs.get(ATTR_ACTIVITY, self._default_activity) activity = kwargs.get(ATTR_ACTIVITY, self._default_activity)
if activity: if activity:
activity_id = None activity_id = None
if activity.isdigit() or activity == '-1': if activity.isdigit() or activity == '-1':
_LOGGER.debug("Activity is numeric") _LOGGER.debug("%s: Activity is numeric", self.name)
if self._client.get_activity_name(int(activity)): if self._client.get_activity_name(int(activity)):
activity_id = activity activity_id = activity
if not activity_id: if activity_id is None:
_LOGGER.debug("Find activity ID based on name") _LOGGER.debug("%s: Find activity ID based on name", self.name)
activity_id = self._client.get_activity_id( activity_id = self._client.get_activity_id(
str(activity).strip()) str(activity).strip())
if not activity_id: if activity_id is None:
_LOGGER.error("Activity %s is invalid", activity) _LOGGER.error("%s: Activity %s is invalid",
self.name, activity)
return return
await self._client.start_activity(activity_id) try:
self._state = True await self._client.start_activity(activity_id)
except aioexc.TimeOut:
_LOGGER.error("%s: Starting activity %s timed-out",
self.name,
activity)
else: else:
_LOGGER.error("No activity specified with turn_on service") _LOGGER.error("%s: No activity specified with turn_on service",
self.name)
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Start the PowerOff activity.""" """Start the PowerOff activity."""
await self._client.power_off() import aioharmony.exceptions as aioexc
_LOGGER.debug("%s: Turn Off", self.name)
try:
await self._client.power_off()
except aioexc.TimeOut:
_LOGGER.error("%s: Powering off timed-out", self.name)
# pylint: disable=arguments-differ # pylint: disable=arguments-differ
async def async_send_command(self, command, **kwargs): async def async_send_command(self, command, **kwargs):
"""Send a list of commands to one device.""" """Send a list of commands to one device."""
from aioharmony.harmonyapi import SendCommandDevice
import aioharmony.exceptions as aioexc
_LOGGER.debug("%s: Send Command", self.name)
device = kwargs.get(ATTR_DEVICE) device = kwargs.get(ATTR_DEVICE)
if device is None: if device is None:
_LOGGER.error("Missing required argument: device") _LOGGER.error("%s: Missing required argument: device", self.name)
return return
device_id = None device_id = None
if device.isdigit(): if device.isdigit():
_LOGGER.debug("Device is numeric") _LOGGER.debug("%s: Device %s is numeric",
self.name, device)
if self._client.get_device_name(int(device)): if self._client.get_device_name(int(device)):
device_id = device device_id = device
if not device_id: if device_id is None:
_LOGGER.debug("Find device ID based on device name") _LOGGER.debug("%s: Find device ID %s based on device name",
device_id = self._client.get_activity_id(str(device).strip()) self.name, device)
device_id = self._client.get_device_id(str(device).strip())
if not device_id: if device_id is None:
_LOGGER.error("Device %s is invalid", device) _LOGGER.error("%s: Device %s is invalid", self.name, device)
return return
num_repeats = kwargs.get(ATTR_NUM_REPEATS) num_repeats = kwargs.get(ATTR_NUM_REPEATS)
delay_secs = kwargs.get(ATTR_DELAY_SECS, self._delay_secs) delay_secs = kwargs.get(ATTR_DELAY_SECS, self._delay_secs)
# Creating list of commands to send.
snd_cmnd_list = []
for _ in range(num_repeats): for _ in range(num_repeats):
for single_command in command: for single_command in command:
_LOGGER.debug("Sending command %s", single_command) send_command = SendCommandDevice(
await self._client.send_command(device, single_command) device=device,
await asyncio.sleep(delay_secs) command=single_command,
delay=0
)
snd_cmnd_list.append(send_command)
if delay_secs > 0:
snd_cmnd_list.append(float(delay_secs))
_LOGGER.debug("%s: Sending commands", self.name)
try:
result_list = await self._client.send_commands(snd_cmnd_list)
except aioexc.TimeOut:
_LOGGER.error("%s: Sending commands timed-out", self.name)
return
for result in result_list:
_LOGGER.error("Sending command %s to device %s failed with code "
"%s: %s",
result.command.command,
result.command.device,
result.command.code,
result.command.msg
)
async def sync(self): async def sync(self):
"""Sync the Harmony device with the web service.""" """Sync the Harmony device with the web service."""
_LOGGER.debug("Syncing hub with Harmony servers") import aioharmony.exceptions as aioexc
await self._client.sync()
await self._client.get_config() _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name)
await self.hass.async_add_executor_job(self.write_config_file) try:
await self._client.sync()
except aioexc.TimeOut:
_LOGGER.error("%s: Syncing hub with Harmony cloud timed-out",
self.name)
else:
await self.hass.async_add_executor_job(self.write_config_file)
def write_config_file(self): def write_config_file(self):
"""Write Harmony configuration file.""" """Write Harmony configuration file."""
_LOGGER.debug("Writing hub config to file: %s", self._config_path) _LOGGER.debug("%s: Writing hub config to file: %s",
self.name,
self._config_path)
if self._client.config is None:
_LOGGER.warning("%s: No configuration received from hub",
self.name)
return
try: try:
with open(self._config_path, 'w+', encoding='utf-8') as file_out: with open(self._config_path, 'w+', encoding='utf-8') as file_out:
json.dump(self._client.json_config, file_out, json.dump(self._client.json_config, file_out,
sort_keys=True, indent=4) sort_keys=True, indent=4)
except IOError as exc: except IOError as exc:
_LOGGER.error("Unable to write HUB configuration to %s: %s", _LOGGER.error("%s: Unable to write HUB configuration to %s: %s",
self._config_path, exc) self.name, self._config_path, exc)

View File

@ -104,6 +104,9 @@ aiofreepybox==0.0.6
# homeassistant.components.camera.yi # homeassistant.components.camera.yi
aioftp==0.12.0 aioftp==0.12.0
# homeassistant.components.remote.harmony
aioharmony==0.1.1
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
@ -523,9 +526,6 @@ homematicip==0.9.8
# homeassistant.components.remember_the_milk # homeassistant.components.remember_the_milk
httplib2==0.10.3 httplib2==0.10.3
# homeassistant.components.remote.harmony
https://github.com/home-assistant/pyharmony/archive/31efd339a3c39e7b8f58e823a0eddb59013e03ae.zip#pyharmony==1.0.21b1
# homeassistant.components.huawei_lte # homeassistant.components.huawei_lte
huawei-lte-api==1.1.1 huawei-lte-api==1.1.1