From 30345489e64faa8aceaf89acbab9800022a9a1cb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 Dec 2018 10:21:24 +0100 Subject: [PATCH 01/13] Updated frontend to 20181211.1 --- 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 8caca591305..fcbfcd5b8c2 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.0'] +REQUIREMENTS = ['home-assistant-frontend==20181211.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index d1c3f266e01..1b1b6106f69 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.0 +home-assistant-frontend==20181211.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3ad8015a4c..f756568e203 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.0 +home-assistant-frontend==20181211.1 # homeassistant.components.homematicip_cloud homematicip==0.9.8 From 65bd308491fc35696ae9dff18f61e096546e3e9b Mon Sep 17 00:00:00 2001 From: Andrew Hayworth Date: Fri, 7 Dec 2018 14:17:34 -0600 Subject: [PATCH 02/13] Set lock status correctly for Schlage BE469 Z-Wave locks (#18737) * Set lock status correctly for Schlage BE469 Z-Wave locks PR #17386 attempted to improve the state of z-wave lock tracking for some problematic models. However, it operated under a flawed assumptions. Namely, that we can always trust `self.values` to have fresh data, and that the Schlage BE469 sends alarm reports after every lock event. We can't trust `self.values`, and the Schlage is very broken. :) When we receive a notification from the driver about a state change, we call `update_properties` - but we can (and do!) have _stale_ properties left over from previous updates. #17386 really works best if you start from a clean slate each time. However, `update_properties` is called on every value update, and we don't get a reason why. Moreover, values that weren't just refreshed are not removed. So blindly looking at something like `self.values.access_control` when deciding to apply a workaround is not going to always be correct - it may or may not be, depending on what happened in the past. For the sad case of the BE469, here are the Z-Wave events that happen under various circumstances: RF Lock / Unlock: - Send: door lock command set - Receive: door lock report - Send: door lock command get - Receive: door lock report Manual lock / Unlock: - Receive: alarm - Send: door lock command get - Receive: door lock report Keypad lock / Unlock: - Receive: alarm - Send: door lock command get - Receive: door lock report Thus, this PR introduces yet another work around - we track the current and last z-wave command that the driver saw, and make assumptions based on the sequence of events. This seems to be the most reliable way to go - simply asking the driver to refresh various states doesn't clear out alarms the way you would expect; this model doesn't support the access control logging commands; and trying to manually clear out alarm state when calling RF lock/unlock was tricky. The lock state, when the z-wave network restarts, may look out of sync for a few minutes. However, after the full network restart is complete, everything looks good in my testing. * Fix linter --- homeassistant/components/lock/zwave.py | 58 ++++++++++++++++++++------ tests/components/lock/test_zwave.py | 46 +++++++++++++++++++- 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 796c62377f1..b4bb233c9cc 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -29,8 +29,9 @@ SERVICE_CLEAR_USERCODE = 'clear_usercode' POLYCONTROL = 0x10E DANALOCK_V2_BTZE = 0x2 POLYCONTROL_DANALOCK_V2_BTZE_LOCK = (POLYCONTROL, DANALOCK_V2_BTZE) -WORKAROUND_V2BTZE = 'v2btze' -WORKAROUND_DEVICE_STATE = 'state' +WORKAROUND_V2BTZE = 1 +WORKAROUND_DEVICE_STATE = 2 +WORKAROUND_TRACK_MESSAGE = 4 DEVICE_MAPPINGS = { POLYCONTROL_DANALOCK_V2_BTZE_LOCK: WORKAROUND_V2BTZE, @@ -43,7 +44,7 @@ DEVICE_MAPPINGS = { # Yale YRD220 (as reported by adrum in PR #17386) (0x0109, 0x0000): WORKAROUND_DEVICE_STATE, # Schlage BE469 - (0x003B, 0x5044): WORKAROUND_DEVICE_STATE, + (0x003B, 0x5044): WORKAROUND_DEVICE_STATE | WORKAROUND_TRACK_MESSAGE, # Schlage FE599NX (0x003B, 0x504C): WORKAROUND_DEVICE_STATE, } @@ -51,13 +52,15 @@ DEVICE_MAPPINGS = { LOCK_NOTIFICATION = { '1': 'Manual Lock', '2': 'Manual Unlock', - '3': 'RF Lock', - '4': 'RF Unlock', '5': 'Keypad Lock', '6': 'Keypad Unlock', '11': 'Lock Jammed', '254': 'Unknown Event' } +NOTIFICATION_RF_LOCK = '3' +NOTIFICATION_RF_UNLOCK = '4' +LOCK_NOTIFICATION[NOTIFICATION_RF_LOCK] = 'RF Lock' +LOCK_NOTIFICATION[NOTIFICATION_RF_UNLOCK] = 'RF Unlock' LOCK_ALARM_TYPE = { '9': 'Deadbolt Jammed', @@ -66,8 +69,6 @@ LOCK_ALARM_TYPE = { '19': 'Unlocked with Keypad by user ', '21': 'Manually Locked ', '22': 'Manually Unlocked ', - '24': 'Locked by RF', - '25': 'Unlocked by RF', '27': 'Auto re-lock', '33': 'User deleted: ', '112': 'Master code changed or User added: ', @@ -79,6 +80,10 @@ LOCK_ALARM_TYPE = { '168': 'Critical Battery Level', '169': 'Battery too low to operate' } +ALARM_RF_LOCK = '24' +ALARM_RF_UNLOCK = '25' +LOCK_ALARM_TYPE[ALARM_RF_LOCK] = 'Locked by RF' +LOCK_ALARM_TYPE[ALARM_RF_UNLOCK] = 'Unlocked by RF' MANUAL_LOCK_ALARM_LEVEL = { '1': 'by Key Cylinder or Inside thumb turn', @@ -229,6 +234,8 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): self._lock_status = None self._v2btze = None self._state_workaround = False + self._track_message_workaround = False + self._previous_message = None # Enable appropriate workaround flags for our device # Make sure that we have values for the key before converting to int @@ -237,26 +244,30 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): specific_sensor_key = (int(self.node.manufacturer_id, 16), int(self.node.product_id, 16)) if specific_sensor_key in DEVICE_MAPPINGS: - if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_V2BTZE: + workaround = DEVICE_MAPPINGS[specific_sensor_key] + if workaround & WORKAROUND_V2BTZE: self._v2btze = 1 _LOGGER.debug("Polycontrol Danalock v2 BTZE " "workaround enabled") - if DEVICE_MAPPINGS[specific_sensor_key] == \ - WORKAROUND_DEVICE_STATE: + if workaround & WORKAROUND_DEVICE_STATE: self._state_workaround = True _LOGGER.debug( "Notification device state workaround enabled") + if workaround & WORKAROUND_TRACK_MESSAGE: + self._track_message_workaround = True + _LOGGER.debug("Message tracking workaround enabled") self.update_properties() def update_properties(self): """Handle data changes for node values.""" self._state = self.values.primary.data - _LOGGER.debug("Lock state set from Bool value and is %s", self._state) + _LOGGER.debug("lock state set to %s", self._state) if self.values.access_control: notification_data = self.values.access_control.data self._notification = LOCK_NOTIFICATION.get(str(notification_data)) if self._state_workaround: self._state = LOCK_STATUS.get(str(notification_data)) + _LOGGER.debug("workaround: lock state set to %s", self._state) if self._v2btze: if self.values.v2btze_advanced and \ self.values.v2btze_advanced.data == CONFIG_ADVANCED: @@ -265,16 +276,37 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): "Lock state set from Access Control value and is %s, " "get=%s", str(notification_data), self.state) + if self._track_message_workaround: + this_message = self.node.stats['lastReceivedMessage'][5] + + if this_message == zwave.const.COMMAND_CLASS_DOOR_LOCK: + self._state = self.values.primary.data + _LOGGER.debug("set state to %s based on message tracking", + self._state) + if self._previous_message == \ + zwave.const.COMMAND_CLASS_DOOR_LOCK: + if self._state: + self._notification = \ + LOCK_NOTIFICATION[NOTIFICATION_RF_LOCK] + self._lock_status = \ + LOCK_ALARM_TYPE[ALARM_RF_LOCK] + else: + self._notification = \ + LOCK_NOTIFICATION[NOTIFICATION_RF_UNLOCK] + self._lock_status = \ + LOCK_ALARM_TYPE[ALARM_RF_UNLOCK] + return + + self._previous_message = this_message + if not self.values.alarm_type: return alarm_type = self.values.alarm_type.data - _LOGGER.debug("Lock alarm_type is %s", str(alarm_type)) if self.values.alarm_level: alarm_level = self.values.alarm_level.data else: alarm_level = None - _LOGGER.debug("Lock alarm_level is %s", str(alarm_level)) if not alarm_type: return diff --git a/tests/components/lock/test_zwave.py b/tests/components/lock/test_zwave.py index 3955538273b..484e4796759 100644 --- a/tests/components/lock/test_zwave.py +++ b/tests/components/lock/test_zwave.py @@ -62,7 +62,7 @@ def test_lock_value_changed(mock_openzwave): assert device.is_locked -def test_lock_value_changed_workaround(mock_openzwave): +def test_lock_state_workaround(mock_openzwave): """Test value changed for Z-Wave lock using notification state.""" node = MockNode(manufacturer_id='0090', product_id='0440') values = MockEntityValues( @@ -78,6 +78,50 @@ def test_lock_value_changed_workaround(mock_openzwave): assert not device.is_locked +def test_track_message_workaround(mock_openzwave): + """Test value changed for Z-Wave lock by alarm-clearing workaround.""" + node = MockNode(manufacturer_id='003B', product_id='5044', + stats={'lastReceivedMessage': [0] * 6}) + values = MockEntityValues( + primary=MockValue(data=True, node=node), + access_control=None, + alarm_type=None, + alarm_level=None, + ) + + # Here we simulate an RF lock. The first zwave.get_device will call + # update properties, simulating the first DoorLock report. We then trigger + # a change, simulating the openzwave automatic refreshing behavior (which + # is enabled for at least the lock that needs this workaround) + node.stats['lastReceivedMessage'][5] = const.COMMAND_CLASS_DOOR_LOCK + device = zwave.get_device(node=node, values=values) + value_changed(values.primary) + assert device.is_locked + assert device.device_state_attributes[zwave.ATTR_NOTIFICATION] == 'RF Lock' + + # Simulate a keypad unlock. We trigger a value_changed() which simulates + # the Alarm notification received from the lock. Then, we trigger + # value_changed() to simulate the automatic refreshing behavior. + values.access_control = MockValue(data=6, node=node) + values.alarm_type = MockValue(data=19, node=node) + values.alarm_level = MockValue(data=3, node=node) + node.stats['lastReceivedMessage'][5] = const.COMMAND_CLASS_ALARM + value_changed(values.access_control) + node.stats['lastReceivedMessage'][5] = const.COMMAND_CLASS_DOOR_LOCK + values.primary.data = False + value_changed(values.primary) + assert not device.is_locked + assert device.device_state_attributes[zwave.ATTR_LOCK_STATUS] == \ + 'Unlocked with Keypad by user 3' + + # Again, simulate an RF lock. + device.lock() + node.stats['lastReceivedMessage'][5] = const.COMMAND_CLASS_DOOR_LOCK + value_changed(values.primary) + assert device.is_locked + assert device.device_state_attributes[zwave.ATTR_NOTIFICATION] == 'RF Lock' + + def test_v2btze_value_changed(mock_openzwave): """Test value changed for v2btze Z-Wave lock.""" node = MockNode(manufacturer_id='010e', product_id='0002') From 45238295dfe4f52d2ec9b8a0e5411f254b9d76b9 Mon Sep 17 00:00:00 2001 From: liaanvdm <43240119+liaanvdm@users.noreply.github.com> Date: Fri, 14 Dec 2018 15:04:04 +0200 Subject: [PATCH 03/13] Fix restore state for manual alarm control panel (#19284) * Fixed manual alarm control panel restore state * Revert "Fixed manual alarm control panel restore state" This reverts commit 61c9faf434a8bb276133578a0811100a796784ca. * Fixed manual alarm control panel's state restore --- .../components/alarm_control_panel/manual.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 0a79d74d686..0bbbd0689e2 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -310,7 +310,15 @@ class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" + await super().async_added_to_hass() state = await self.async_get_last_state() if state: - self._state = state.state - self._state_ts = state.last_updated + if state.state == STATE_ALARM_PENDING and \ + hasattr(state, 'attributes') and \ + state.attributes['pre_pending_state']: + # If in pending state, we return to the pre_pending_state + self._state = state.attributes['pre_pending_state'] + self._state_ts = dt_util.utcnow() + else: + self._state = state.state + self._state_ts = state.last_updated From f613cd38fcf93468524a013d4454d18ddbf2476f Mon Sep 17 00:00:00 2001 From: Glen Takahashi Date: Sun, 16 Dec 2018 13:06:27 -0500 Subject: [PATCH 04/13] Fix not being able to update entities (#19344) When editing an entity in the frontend dialog, pressing save causes a "save failed: Entity is already registered" error. This is because the frontend always sets `name` and `new_entity_id` in the websocket command even if they haven't been changed. This adds a check that the `new_entity_id` is actually different from `entity_id` before erroring that the `new_entity_id` is already registered. --- homeassistant/components/config/entity_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 71833a2e42d..fdac1ad95da 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -104,7 +104,7 @@ async def websocket_update_entity(hass, connection, msg): if 'name' in msg: changes['name'] = msg['name'] - if 'new_entity_id' in msg: + if 'new_entity_id' in msg and msg['new_entity_id'] != msg['entity_id']: changes['new_entity_id'] = msg['new_entity_id'] if hass.states.get(msg['new_entity_id']) is not None: connection.send_message(websocket_api.error_message( From 23cc4d145357bbc6b19b31c48ffac5d65fb53b7e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 Dec 2018 10:46:39 +0100 Subject: [PATCH 05/13] Bumped version to 0.84.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 133df0fdf88..c20621c43a2 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 = '2' +PATCH_VERSION = '3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From a7e98f12f463a1fd290d52f4ba3d7b93d1034622 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 19 Dec 2018 14:04:08 +0100 Subject: [PATCH 06/13] 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 07/13] 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 08/13] 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 09/13] 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) From 4c1d978aa4a018bf83638aa28d8799edada91618 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 19 Dec 2018 15:41:14 +0100 Subject: [PATCH 10/13] Bump pyharmony (#19460) --- homeassistant/components/remote/harmony.py | 6 +++--- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 89179db1cf6..0200a684099 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -24,9 +24,9 @@ from homeassistant.util import slugify # REQUIREMENTS = ['pyharmony==1.0.22'] REQUIREMENTS = [ - 'https://github.com/home-assistant//pyharmony/archive/' - '4b27f8a35ea61123ef531ad078a4357cc26b00db.zip' - '#pyharmony==1.0.21b0' + 'https://github.com/home-assistant/pyharmony/archive/' + '31efd339a3c39e7b8f58e823a0eddb59013e03ae.zip' + '#pyharmony==1.0.21b1' ] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index c7831672381..b09c97ede3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -509,7 +509,7 @@ homematicip==0.9.8 httplib2==0.10.3 # homeassistant.components.remote.harmony -https://github.com/home-assistant//pyharmony/archive/4b27f8a35ea61123ef531ad078a4357cc26b00db.zip#pyharmony==1.0.21b0 +https://github.com/home-assistant/pyharmony/archive/31efd339a3c39e7b8f58e823a0eddb59013e03ae.zip#pyharmony==1.0.21b1 # homeassistant.components.huawei_lte huawei-lte-api==1.0.16 From 264d18bc8374914651b1e7297b22dcb7c680b64a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 19 Dec 2018 15:42:02 +0100 Subject: [PATCH 11/13] Bumped version to 0.84.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 405dcd028a0..b9ec0d96964 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 = '4' +PATCH_VERSION = '5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From ec28ee3c42da2aebba472505921d8bf3723a9df5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 21 Dec 2018 11:23:05 +0100 Subject: [PATCH 12/13] Remove check if base url is local (#19494) --- homeassistant/helpers/config_entry_flow.py | 12 ------------ tests/helpers/test_config_entry_flow.py | 13 ------------- 2 files changed, 25 deletions(-) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 31d9907d315..8f5705bc67a 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -1,10 +1,7 @@ """Helpers for data entry flows for config entries.""" from functools import partial -from ipaddress import ip_address -from urllib.parse import urlparse from homeassistant import config_entries -from homeassistant.util.network import is_local def register_discovery_flow(domain, title, discovery_function, @@ -115,15 +112,6 @@ class WebhookFlowHandler(config_entries.ConfigFlow): if not self._allow_multiple and self._async_current_entries(): return self.async_abort(reason='one_instance_allowed') - try: - url_parts = urlparse(self.hass.config.api.base_url) - - if is_local(ip_address(url_parts.hostname)): - return self.async_abort(reason='not_internet_accessible') - except ValueError: - # If it's not an IP address, it's very likely publicly accessible - pass - if user_input is None: return self.async_show_form( step_id='user', diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 8e38f76f1c0..846c2cd1560 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -183,19 +183,6 @@ async def test_webhook_multiple_entries_allowed(hass, webhook_flow_conf): assert result['type'] == data_entry_flow.RESULT_TYPE_FORM -async def test_webhook_config_flow_aborts_external_url(hass, - webhook_flow_conf): - """Test configuring a webhook without an external url.""" - flow = config_entries.HANDLERS['test_single']() - flow.hass = hass - - hass.config.api = Mock(base_url='http://192.168.1.10') - result = await flow.async_step_user() - - assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT - assert result['reason'] == 'not_internet_accessible' - - async def test_webhook_config_flow_registers_webhook(hass, webhook_flow_conf): """Test setting up an entry creates a webhook.""" flow = config_entries.HANDLERS['test_single']() From 0471e15c2823e679bbdbfdb92cab3925e182e3e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 21 Dec 2018 14:04:54 +0100 Subject: [PATCH 13/13] Bumped version to 0.84.6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b9ec0d96964..ceed012eb9b 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 = '5' +PATCH_VERSION = '6' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)