diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 0200a6840993..aeb3d85d91c4 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -7,32 +7,28 @@ import asyncio import json import logging -from datetime import timedelta -from pathlib import Path import voluptuous as vol from homeassistant.components import remote from homeassistant.components.remote import ( 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 ( - 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 from homeassistant.exceptions import PlatformNotReady from homeassistant.util import slugify -# REQUIREMENTS = ['pyharmony==1.0.22'] -REQUIREMENTS = [ - 'https://github.com/home-assistant/pyharmony/archive/' - '31efd339a3c39e7b8f58e823a0eddb59013e03ae.zip' - '#pyharmony==1.0.21b1' -] +REQUIREMENTS = ['aioharmony==0.1.1'] _LOGGER = logging.getLogger(__name__) +ATTR_CURRENT_ACTIVITY = 'current_activity' + DEFAULT_PORT = 8088 -SCAN_INTERVAL = timedelta(seconds=5) DEVICES = [] CONF_DEVICE_CACHE = 'harmony_device_cache' @@ -55,7 +51,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Harmony platform.""" - host = None activity = None 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 override = next((c for c in hass.data[CONF_DEVICE_CACHE] if c.get(CONF_NAME) == discovery_info.get(CONF_NAME)), - False) + None) port = DEFAULT_PORT delay_secs = DEFAULT_DELAY_SECS - if override: + if override is not None: activity = override.get(ATTR_ACTIVITY) delay_secs = override.get(ATTR_DELAY_SECS) 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: await service_func(device, *service_func_args) - device.schedule_update_ha_state(True) async def _sync_service(service): @@ -142,39 +136,53 @@ class HarmonyRemote(remote.RemoteDevice): def __init__(self, name, host, port, activity, out_path, delay_secs): """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.host = host self.port = port self._state = None self._current_activity = None self._default_activity = activity - # self._client = pyharmony.get_client(host, port, self.new_activity) - self._client = harmony_client.HarmonyClient(host) + self._client = HarmonyClient( + 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._delay_secs = delay_secs - _LOGGER.debug("HarmonyRemote device init completed for: %s", name) + self._available = False async def async_added_to_hass(self): """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.""" - 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) - _LOGGER.debug("Connecting.") - await self._client.connect() - await self._client.get_config() - if not Path(self._config_path).is_file(): - self.write_config_file() - - # Poll for initial state - self.new_activity(await self._client.get_current_activity()) + _LOGGER.debug("%s: Connecting", self._name) + try: + await self._client.connect() + except aioexc.TimeOut: + _LOGGER.error("%s: Connection timed-out", self._name) + else: + # Set initial state + self.new_activity(self._client.current_activity) @property def name(self): @@ -184,113 +192,189 @@ def name(self): @property def should_poll(self): """Return the fact that we should not be polled.""" - return True + return False @property def device_state_attributes(self): """Add platform specific attributes.""" - return {'current_activity': self._current_activity} + return {ATTR_CURRENT_ACTIVITY: self._current_activity} @property def is_on(self): """Return False if PowerOff is the current activity, otherwise True.""" return self._current_activity not in [None, 'PowerOff'] - async def async_update(self): - """Retrieve current activity from Hub.""" - _LOGGER.debug("Updating Harmony.") - if not self._client.config: - await self._client.get_config() + @property + def available(self): + """Return True if connected to Hub, otherwise False.""" + return self._available - activity_id = await self._client.get_current_activity() - activity_name = self._client.get_activity_name(activity_id) - _LOGGER.debug("%s activity reported as: %s", self._name, activity_name) + def new_activity(self, activity_info: tuple) -> None: + """Call for updating the current activity.""" + activity_id, activity_name = activity_info + _LOGGER.debug("%s: activity reported as: %s", self._name, + activity_name) self._current_activity = activity_name - self._state = bool(self._current_activity != 'PowerOff') - return + self._state = bool(activity_id != -1) + self.async_schedule_update_ha_state() - def new_activity(self, activity_id): + async def new_config(self, _=None): """Call for updating the current activity.""" - 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') - self.schedule_update_ha_state() + _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): """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) if activity: activity_id = None 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)): activity_id = activity - if not activity_id: - _LOGGER.debug("Find activity ID based on name") + if activity_id is None: + _LOGGER.debug("%s: Find activity ID based on name", self.name) activity_id = self._client.get_activity_id( str(activity).strip()) - if not activity_id: - _LOGGER.error("Activity %s is invalid", activity) + if activity_id is None: + _LOGGER.error("%s: Activity %s is invalid", + self.name, activity) return - await self._client.start_activity(activity_id) - self._state = True + try: + await self._client.start_activity(activity_id) + except aioexc.TimeOut: + _LOGGER.error("%s: Starting activity %s timed-out", + self.name, + activity) 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): """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 async def async_send_command(self, command, **kwargs): """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) if device is None: - _LOGGER.error("Missing required argument: device") + _LOGGER.error("%s: Missing required argument: device", self.name) return device_id = None 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)): device_id = device - if not device_id: - _LOGGER.debug("Find device ID based on device name") - device_id = self._client.get_activity_id(str(device).strip()) + if device_id is None: + _LOGGER.debug("%s: Find device ID %s based on device name", + self.name, device) + device_id = self._client.get_device_id(str(device).strip()) - if not device_id: - _LOGGER.error("Device %s is invalid", device) + if device_id is None: + _LOGGER.error("%s: Device %s is invalid", self.name, device) return num_repeats = kwargs.get(ATTR_NUM_REPEATS) 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 single_command in command: - _LOGGER.debug("Sending command %s", single_command) - await self._client.send_command(device, single_command) - await asyncio.sleep(delay_secs) + send_command = SendCommandDevice( + device=device, + 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): """Sync the Harmony device with the web service.""" - _LOGGER.debug("Syncing hub with Harmony servers") - await self._client.sync() - await self._client.get_config() - await self.hass.async_add_executor_job(self.write_config_file) + import aioharmony.exceptions as aioexc + + _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name) + 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): """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: with open(self._config_path, 'w+', encoding='utf-8') as file_out: json.dump(self._client.json_config, file_out, sort_keys=True, indent=4) except IOError as exc: - _LOGGER.error("Unable to write HUB configuration to %s: %s", - self._config_path, exc) + _LOGGER.error("%s: Unable to write HUB configuration to %s: %s", + self.name, self._config_path, exc) diff --git a/requirements_all.txt b/requirements_all.txt index e516b2c06319..9eda30f90554 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -104,6 +104,9 @@ aiofreepybox==0.0.6 # homeassistant.components.camera.yi aioftp==0.12.0 +# homeassistant.components.remote.harmony +aioharmony==0.1.1 + # homeassistant.components.emulated_hue # homeassistant.components.http aiohttp_cors==0.7.0 @@ -523,9 +526,6 @@ homematicip==0.9.8 # homeassistant.components.remember_the_milk 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 huawei-lte-api==1.1.1