From a7e98f12f463a1fd290d52f4ba3d7b93d1034622 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 19 Dec 2018 14:04:08 +0100 Subject: [PATCH 1/4] Updated frontend to 20181211.2 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index fcbfcd5b8c2..eeb11208dd1 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20181211.1'] +REQUIREMENTS = ['home-assistant-frontend==20181211.2'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 1b1b6106f69..c6361d1b2f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ hole==0.3.0 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181211.1 +home-assistant-frontend==20181211.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f756568e203..045d6fe3f26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -101,7 +101,7 @@ hdate==0.7.5 holidays==0.9.8 # homeassistant.components.frontend -home-assistant-frontend==20181211.1 +home-assistant-frontend==20181211.2 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 257a91d929b2107265a6984ad7f4c5b363070628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20L=C3=BCneborg?= <43782170+mopolus@users.noreply.github.com> Date: Tue, 18 Dec 2018 12:40:03 +0100 Subject: [PATCH 2/4] Fix IHC config schema (#19415) * Update __init__.py Update "unit" -> "unit_of_measurement" and configuration (from plural to singular) * Update __init__.py * Removing vol.ALLOW_EXTRA arguments * Update __init__.py --- homeassistant/components/ihc/__init__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 052921ad37a..16c51c00767 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -18,8 +18,8 @@ from homeassistant.components.ihc.const import ( SERVICE_SET_RUNTIME_VALUE_FLOAT, SERVICE_SET_RUNTIME_VALUE_INT) from homeassistant.config import load_yaml_config_file from homeassistant.const import ( - CONF_BINARY_SENSORS, CONF_ID, CONF_LIGHTS, CONF_NAME, CONF_PASSWORD, - CONF_SENSORS, CONF_SWITCHES, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_URL, + CONF_ID, CONF_NAME, CONF_PASSWORD, + CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_URL, CONF_USERNAME, TEMP_CELSIUS) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -49,7 +49,7 @@ DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_POSITION): cv.string, vol.Optional(CONF_NOTE): cv.string -}, extra=vol.ALLOW_EXTRA) +}) SWITCH_SCHEMA = DEVICE_SCHEMA.extend({ @@ -75,31 +75,31 @@ IHC_SCHEMA = vol.Schema({ vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_AUTOSETUP, default=True): cv.boolean, vol.Optional(CONF_INFO, default=True): cv.boolean, - vol.Optional(CONF_BINARY_SENSORS, default=[]): + vol.Optional(CONF_BINARY_SENSOR, default=[]): vol.All(cv.ensure_list, [ vol.All( BINARY_SENSOR_SCHEMA, validate_name) ]), - vol.Optional(CONF_LIGHTS, default=[]): + vol.Optional(CONF_LIGHT, default=[]): vol.All(cv.ensure_list, [ vol.All( LIGHT_SCHEMA, validate_name) ]), - vol.Optional(CONF_SENSORS, default=[]): + vol.Optional(CONF_SENSOR, default=[]): vol.All(cv.ensure_list, [ vol.All( SENSOR_SCHEMA, validate_name) ]), - vol.Optional(CONF_SWITCHES, default=[]): + vol.Optional(CONF_SWITCH, default=[]): vol.All(cv.ensure_list, [ vol.All( SWITCH_SCHEMA, validate_name) ]), -}, extra=vol.ALLOW_EXTRA) +}) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema(vol.All( @@ -224,7 +224,8 @@ def get_manual_configuration(hass, config, conf, ihc_controller, 'type': sensor_cfg.get(CONF_TYPE), 'inverting': sensor_cfg.get(CONF_INVERTING), 'dimmable': sensor_cfg.get(CONF_DIMMABLE), - 'unit': sensor_cfg.get(CONF_UNIT_OF_MEASUREMENT) + 'unit_of_measurement': sensor_cfg.get( + CONF_UNIT_OF_MEASUREMENT) } } discovery_info[name] = device From ff1dba35291472df6fbc6e6680c6060b3dbcec67 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Wed, 19 Dec 2018 06:21:40 -0700 Subject: [PATCH 3/4] Use web sockets for Harmony HUB (#19440) * Updates to Harmony for web sockets Updates to harmony to use web sockets with async * Lint * Small fixes * Fix send_command Continued improvements: -) Fixed send_command -) Get HUB configuration during update in case it was not retrieved earlier (i.e. HUB unavailable) * Further improvements Completely removed dependency on __main__ for pyharmony, instead everything is now done from the HarmonyClient class. Writing out Harmony configuration file as a JSON file. Using same functionality to determine if activity provided is an ID or name for device, allowing send_command to receive a device ID or device name. * Point requirements to updated pyharmony repo Updated REQUIREMENTS to point to repository containing the updates for pyharmony. * lint lint * Small fix for device and activity ID Small fix in checking if provided device or activity ID is valid. * Pin package version * No I/O in event loop * Point at HA fork with correct version bump * Fix req --- homeassistant/components/remote/harmony.py | 145 +++++++++++++++------ requirements_all.txt | 6 +- script/gen_requirements_all.py | 2 +- 3 files changed, 109 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 14008d49760..89179db1cf6 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -4,8 +4,11 @@ Support for Harmony Hub devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/remote.harmony/ """ +import asyncio +import json import logging -import time +from datetime import timedelta +from pathlib import Path import voluptuous as vol @@ -19,11 +22,17 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady from homeassistant.util import slugify -REQUIREMENTS = ['pyharmony==1.0.20'] +# REQUIREMENTS = ['pyharmony==1.0.22'] +REQUIREMENTS = [ + 'https://github.com/home-assistant//pyharmony/archive/' + '4b27f8a35ea61123ef531ad078a4357cc26b00db.zip' + '#pyharmony==1.0.21b0' +] _LOGGER = logging.getLogger(__name__) -DEFAULT_PORT = 5222 +DEFAULT_PORT = 8088 +SCAN_INTERVAL = timedelta(seconds=5) DEVICES = [] CONF_DEVICE_CACHE = 'harmony_device_cache' @@ -43,7 +52,8 @@ HARMONY_SYNC_SCHEMA = vol.Schema({ }) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Harmony platform.""" host = None activity = None @@ -95,7 +105,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device = HarmonyRemote( name, address, port, activity, harmony_conf_file, delay_secs) DEVICES.append(device) - add_entities([device]) + async_add_entities([device]) register_services(hass) except (ValueError, AttributeError): raise PlatformNotReady @@ -103,12 +113,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def register_services(hass): """Register all services for harmony devices.""" - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_SYNC, _sync_service, schema=HARMONY_SYNC_SCHEMA) -def _apply_service(service, service_func, *service_func_args): +async def _apply_service(service, service_func, *service_func_args): """Handle services to apply.""" entity_ids = service.data.get('entity_id') @@ -119,12 +129,12 @@ def _apply_service(service, service_func, *service_func_args): _devices = DEVICES for device in _devices: - service_func(device, *service_func_args) + await service_func(device, *service_func_args) device.schedule_update_ha_state(True) -def _sync_service(service): - _apply_service(service, HarmonyRemote.sync) +async def _sync_service(service): + await _apply_service(service, HarmonyRemote.sync) class HarmonyRemote(remote.RemoteDevice): @@ -132,8 +142,7 @@ class HarmonyRemote(remote.RemoteDevice): def __init__(self, name, host, port, activity, out_path, delay_secs): """Initialize HarmonyRemote class.""" - import pyharmony - from pathlib import Path + import pyharmony.client as harmony_client _LOGGER.debug("HarmonyRemote device init started for: %s", name) self._name = name @@ -142,23 +151,30 @@ class HarmonyRemote(remote.RemoteDevice): self._state = None self._current_activity = None self._default_activity = activity - self._client = pyharmony.get_client(host, port, self.new_activity) + # self._client = pyharmony.get_client(host, port, self.new_activity) + self._client = harmony_client.HarmonyClient(host) self._config_path = out_path - 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 + _LOGGER.debug("HarmonyRemote device init completed for: %s", name) async 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)) + _LOGGER.debug("HarmonyRemote added for: %s", self._name) + + async def shutdown(event): + """Close connection on shutdown.""" + await self._client.disconnect() + + 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(self._client.get_current_activity()) + self.new_activity(await self._client.get_current_activity()) @property def name(self): @@ -168,7 +184,7 @@ class HarmonyRemote(remote.RemoteDevice): @property def should_poll(self): """Return the fact that we should not be polled.""" - return False + return True @property def device_state_attributes(self): @@ -180,52 +196,101 @@ class HarmonyRemote(remote.RemoteDevice): """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() + + 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) + 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.""" - import pyharmony - activity_name = pyharmony.activity_name(self._config, activity_id) + 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() - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Start an activity from the Harmony device.""" - import pyharmony activity = kwargs.get(ATTR_ACTIVITY, self._default_activity) if activity: - activity_id = pyharmony.activity_id(self._config, activity) - self._client.start_activity(activity_id) + activity_id = None + if activity.isdigit() or activity == '-1': + _LOGGER.debug("Activity is numeric") + if self._client.get_activity_name(int(activity)): + activity_id = activity + + if not activity_id: + _LOGGER.debug("Find activity ID based on name") + activity_id = self._client.get_activity_id( + str(activity).strip()) + + if not activity_id: + _LOGGER.error("Activity %s is invalid", activity) + return + + await self._client.start_activity(activity_id) self._state = True else: _LOGGER.error("No activity specified with turn_on service") - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Start the PowerOff activity.""" - self._client.power_off() + await self._client.power_off() # pylint: disable=arguments-differ - def send_command(self, commands, **kwargs): + async def async_send_command(self, command, **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 + device_id = None + if device.isdigit(): + _LOGGER.debug("Device is numeric") + 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 not device_id: + _LOGGER.error("Device %s is invalid", device) + return + 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) + 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) - def sync(self): + async def sync(self): """Sync the Harmony device with the web service.""" - import pyharmony _LOGGER.debug("Syncing hub with Harmony servers") - self._client.sync() - self._config = self._client.get_config() + await self._client.sync() + await self._client.get_config() + 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) - pyharmony.ha_write_config_file(self._config, self._config_path) + 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) diff --git a/requirements_all.txt b/requirements_all.txt index c6361d1b2f6..c7831672381 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -508,6 +508,9 @@ homematicip==0.9.8 # homeassistant.components.remember_the_milk httplib2==0.10.3 +# homeassistant.components.remote.harmony +https://github.com/home-assistant//pyharmony/archive/4b27f8a35ea61123ef531ad078a4357cc26b00db.zip#pyharmony==1.0.21b0 + # homeassistant.components.huawei_lte huawei-lte-api==1.0.16 @@ -965,9 +968,6 @@ pygogogate2==0.1.1 # homeassistant.components.sensor.gtfs pygtfs-homeassistant==0.1.3.dev0 -# homeassistant.components.remote.harmony -pyharmony==1.0.20 - # homeassistant.components.sensor.version pyhaversion==2.0.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 82dab374e42..cc81341e91b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -209,7 +209,7 @@ def gather_modules(): for req in module.REQUIREMENTS: if req in IGNORE_REQ: continue - if '://' in req: + if '://' in req and 'pyharmony' not in req: errors.append( "{}[Only pypi dependencies are allowed: {}]".format( package, req)) From 2b82830eb10dfa3188c33247b3fe81479870e6bb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 19 Dec 2018 14:23:07 +0100 Subject: [PATCH 4/4] Bumped version to 0.84.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c20621c43a2..405dcd028a0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 84 -PATCH_VERSION = '3' +PATCH_VERSION = '4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)