From a17e60208dc9ae7b3915a5b93629489a87e31806 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Mar 2018 14:15:31 -0700 Subject: [PATCH 001/136] Update translations --- .../.translations/nl.json | 3 ++- .../.translations/vi.json | 24 +++++++++++++++++++ .../components/hue/.translations/de.json | 2 +- .../components/hue/.translations/pl.json | 2 +- .../sensor/.translations/season.vi.json | 8 +++++++ 5 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/config_entry_example/.translations/vi.json create mode 100644 homeassistant/components/sensor/.translations/season.vi.json diff --git a/homeassistant/components/config_entry_example/.translations/nl.json b/homeassistant/components/config_entry_example/.translations/nl.json index 10469dd0804..7b52ac88cf0 100644 --- a/homeassistant/components/config_entry_example/.translations/nl.json +++ b/homeassistant/components/config_entry_example/.translations/nl.json @@ -18,6 +18,7 @@ "description": "Voer een naam in voor het testen van de entiteit.", "title": "Naam van de entiteit" } - } + }, + "title": "Voorbeeld van de config vermelding" } } \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/vi.json b/homeassistant/components/config_entry_example/.translations/vi.json new file mode 100644 index 00000000000..e40c4d38e9f --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/vi.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng kh\u00f4ng h\u1ee3p l\u1ec7" + }, + "step": { + "init": { + "data": { + "object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng" + }, + "description": "Xin vui l\u00f2ng nh\u1eadp m\u1ed9t object_id cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", + "title": "Ch\u1ecdn id \u0111\u1ed1i t\u01b0\u1ee3ng" + }, + "name": { + "data": { + "name": "T\u00ean" + }, + "description": "Xin vui l\u00f2ng nh\u1eadp t\u00ean cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", + "title": "T\u00ean c\u1ee7a th\u1ef1c th\u1ec3" + } + }, + "title": "V\u00ed d\u1ee5 v\u1ec1 c\u1ea5u h\u00ecnh th\u1ef1c th\u1ec3" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index b7094d91528..f11af7756c7 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -6,7 +6,7 @@ "no_bridges": "Philips Hue Bridges entdeckt" }, "error": { - "linking": "Unbekannte Link-Fehler aufgetreten.", + "linking": "Unbekannter Link-Fehler aufgetreten.", "register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut" }, "step": { diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index cdd26a5b4b2..e364b7033a1 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -17,7 +17,7 @@ "title": "Wybierz mostek Hue" }, "link": { - "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant. ", + "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant.", "title": "Hub Link" } }, diff --git a/homeassistant/components/sensor/.translations/season.vi.json b/homeassistant/components/sensor/.translations/season.vi.json new file mode 100644 index 00000000000..a3bb21dee27 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.vi.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "M\u00f9a thu", + "spring": "M\u00f9a xu\u00e2n", + "summer": "M\u00f9a h\u00e8", + "winter": "M\u00f9a \u0111\u00f4ng" + } +} \ No newline at end of file From b159484a79b769b280884ed1ceb5f595d149c671 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Mar 2018 14:16:17 -0700 Subject: [PATCH 002/136] Version bump to 0.67.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4ce2f503ad6..d286aa85458 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 66 +MINOR_VERSION = 67 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 872b6cf16b458a8b69abc32fc45bcbbd6392846e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 23 Mar 2018 23:22:33 +0100 Subject: [PATCH 003/136] Updates default Pilight port number (#13419) --- homeassistant/components/pilight.py | 2 +- tests/components/test_pilight.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pilight.py b/homeassistant/components/pilight.py index 71e8232e8c2..344c750c0ec 100644 --- a/homeassistant/components/pilight.py +++ b/homeassistant/components/pilight.py @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) CONF_SEND_DELAY = 'send_delay' DEFAULT_HOST = '127.0.0.1' -DEFAULT_PORT = 5000 +DEFAULT_PORT = 5001 DEFAULT_SEND_DELAY = 0.0 DOMAIN = 'pilight' diff --git a/tests/components/test_pilight.py b/tests/components/test_pilight.py index 06ad84e7a34..24052a56839 100644 --- a/tests/components/test_pilight.py +++ b/tests/components/test_pilight.py @@ -81,7 +81,7 @@ class TestPilight(unittest.TestCase): @patch('homeassistant.components.pilight._LOGGER.error') def test_connection_failed_error(self, mock_error): - """Try to connect at 127.0.0.1:5000 with socket error.""" + """Try to connect at 127.0.0.1:5001 with socket error.""" with assert_setup_component(4): with patch('pilight.pilight.Client', side_effect=socket.error) as mock_client: @@ -93,7 +93,7 @@ class TestPilight(unittest.TestCase): @patch('homeassistant.components.pilight._LOGGER.error') def test_connection_timeout_error(self, mock_error): - """Try to connect at 127.0.0.1:5000 with socket timeout.""" + """Try to connect at 127.0.0.1:5001 with socket timeout.""" with assert_setup_component(4): with patch('pilight.pilight.Client', side_effect=socket.timeout) as mock_client: From 8bd5f66c5731e5ab1fa42886ea5435cf88cf0e20 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 23 Mar 2018 23:50:32 +0100 Subject: [PATCH 004/136] Upgrade mypy to 0.580 (#13420) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index fc9e113e97c..afcdec23a00 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 mock-open==1.3.1 -mypy==0.570 +mypy==0.580 pydocstyle==1.1.1 pylint==1.8.2 pytest-aiohttp==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2c1df2d3bf..02505138343 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 mock-open==1.3.1 -mypy==0.570 +mypy==0.580 pydocstyle==1.1.1 pylint==1.8.2 pytest-aiohttp==0.3.0 From 4d52875229b1709f52470354e9fbd06d38c8d17c Mon Sep 17 00:00:00 2001 From: ChristianKuehnel Date: Sat, 24 Mar 2018 12:16:49 +0100 Subject: [PATCH 005/136] Update to new "b2vapi" of BMW ConnectedDrive (#13305) * updated to new "b2vapi" of bimmer_connected * updated requirements_all.txt * updated 2 more vehicle names after rebase * cleanup of import statements * found one more broken name... * removed unused constant * cleanup of import statements 2 --- .../binary_sensor/bmw_connected_drive.py | 13 +++++---- .../components/bmw_connected_drive.py | 28 ++++++++++--------- .../device_tracker/bmw_connected_drive.py | 6 ++-- .../components/lock/bmw_connected_drive.py | 20 +++++++------ .../components/sensor/bmw_connected_drive.py | 12 ++++---- requirements_all.txt | 2 +- 6 files changed, 44 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index 0f3edd86dcd..e7af5af988b 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -7,8 +7,8 @@ https://home-assistant.io/components/binary_sensor.bmw_connected_drive/ import asyncio import logging -from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN DEPENDENCIES = ['bmw_connected_drive'] @@ -45,7 +45,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._sensor_name = sensor_name self._device_class = device_class self._state = None @@ -75,7 +75,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): """Return the state attributes of the binary sensor.""" vehicle_state = self._vehicle.state result = { - 'car': self._vehicle.modelName + 'car': self._vehicle.name } if self._attribute == 'lids': @@ -91,6 +91,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): def update(self): """Read new state data from the library.""" + from bimmer_connected.state import LockState vehicle_state = self._vehicle.state # device class opening: On means open, Off means closed @@ -101,9 +102,9 @@ class BMWConnectedDriveSensor(BinarySensorDevice): self._state = not vehicle_state.all_windows_closed # device class safety: On means unsafe, Off means safe if self._attribute == 'door_lock_state': - # Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED - self._state = bool(vehicle_state.door_lock_state.value - in ('SELECTIVELOCKED', 'UNLOCKED')) + # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED + self._state = vehicle_state.door_lock_state not in \ + [LockState.LOCKED, LockState.SECURED] def update_callback(self): """Schedule a state update.""" diff --git a/homeassistant/components/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive.py index 9e9e2bafac5..48452b6d79b 100644 --- a/homeassistant/components/bmw_connected_drive.py +++ b/homeassistant/components/bmw_connected_drive.py @@ -4,30 +4,29 @@ Reads vehicle status from BMW connected drive portal. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/bmw_connected_drive/ """ -import logging import datetime +import logging import voluptuous as vol + +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers import discovery from homeassistant.helpers.event import track_utc_time_change - import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD -) -REQUIREMENTS = ['bimmer_connected==0.4.1'] +REQUIREMENTS = ['bimmer_connected==0.5.0'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'bmw_connected_drive' -CONF_VALUES = 'values' -CONF_COUNTRY = 'country' +CONF_REGION = 'region' + ACCOUNT_SCHEMA = vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_COUNTRY): cv.string, + vol.Required(CONF_REGION): vol.Any('north_america', 'china', + 'rest_of_world'), }) CONFIG_SCHEMA = vol.Schema({ @@ -47,9 +46,9 @@ def setup(hass, config): for name, account_config in config[DOMAIN].items(): username = account_config[CONF_USERNAME] password = account_config[CONF_PASSWORD] - country = account_config[CONF_COUNTRY] + region = account_config[CONF_REGION] _LOGGER.debug('Adding new account %s', name) - bimmer = BMWConnectedDriveAccount(username, password, country, name) + bimmer = BMWConnectedDriveAccount(username, password, region, name) accounts.append(bimmer) # update every UPDATE_INTERVAL minutes, starting now @@ -75,12 +74,15 @@ def setup(hass, config): class BMWConnectedDriveAccount(object): """Representation of a BMW vehicle.""" - def __init__(self, username: str, password: str, country: str, + def __init__(self, username: str, password: str, region_str: str, name: str) -> None: """Constructor.""" from bimmer_connected.account import ConnectedDriveAccount + from bimmer_connected.country_selector import get_region_from_name - self.account = ConnectedDriveAccount(username, password, country) + region = get_region_from_name(region_str) + + self.account = ConnectedDriveAccount(username, password, region) self.name = name self._update_listeners = [] diff --git a/homeassistant/components/device_tracker/bmw_connected_drive.py b/homeassistant/components/device_tracker/bmw_connected_drive.py index 6ba2681e4cd..1e501c0e199 100644 --- a/homeassistant/components/device_tracker/bmw_connected_drive.py +++ b/homeassistant/components/device_tracker/bmw_connected_drive.py @@ -37,15 +37,15 @@ class BMWDeviceTracker(object): def update(self) -> None: """Update the device info.""" - dev_id = slugify(self.vehicle.modelName) + dev_id = slugify(self.vehicle.name) _LOGGER.debug('Updating %s', dev_id) attrs = { 'trackr_id': dev_id, 'id': dev_id, - 'name': self.vehicle.modelName + 'name': self.vehicle.name } self._see( - dev_id=dev_id, host_name=self.vehicle.modelName, + dev_id=dev_id, host_name=self.vehicle.name, gps=self.vehicle.state.gps_position, attributes=attrs, icon='mdi:car' ) diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py index c500e02b2f7..c992bf1225a 100644 --- a/homeassistant/components/lock/bmw_connected_drive.py +++ b/homeassistant/components/lock/bmw_connected_drive.py @@ -37,7 +37,7 @@ class BMWLock(LockDevice): self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._sensor_name = sensor_name self._state = None @@ -59,7 +59,7 @@ class BMWLock(LockDevice): """Return the state attributes of the lock.""" vehicle_state = self._vehicle.state return { - 'car': self._vehicle.modelName, + 'car': self._vehicle.name, 'door_lock_state': vehicle_state.door_lock_state.value } @@ -70,7 +70,7 @@ class BMWLock(LockDevice): def lock(self, **kwargs): """Lock the car.""" - _LOGGER.debug("%s: locking doors", self._vehicle.modelName) + _LOGGER.debug("%s: locking doors", self._vehicle.name) # Optimistic state set here because it takes some time before the # update callback response self._state = STATE_LOCKED @@ -79,7 +79,7 @@ class BMWLock(LockDevice): def unlock(self, **kwargs): """Unlock the car.""" - _LOGGER.debug("%s: unlocking doors", self._vehicle.modelName) + _LOGGER.debug("%s: unlocking doors", self._vehicle.name) # Optimistic state set here because it takes some time before the # update callback response self._state = STATE_UNLOCKED @@ -88,13 +88,17 @@ class BMWLock(LockDevice): def update(self): """Update state of the lock.""" - _LOGGER.debug("%s: updating data for %s", self._vehicle.modelName, + from bimmer_connected.state import LockState + + _LOGGER.debug("%s: updating data for %s", self._vehicle.name, self._attribute) vehicle_state = self._vehicle.state - # Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED - self._state = (STATE_LOCKED if vehicle_state.door_lock_state.value - in ('LOCKED', 'SECURED') else STATE_UNLOCKED) + # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED + self._state = STATE_LOCKED \ + if vehicle_state.door_lock_state \ + in [LockState.LOCKED, LockState.SECURED] \ + else STATE_UNLOCKED def update_callback(self): """Schedule a state update.""" diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index 3208c7377df..bd582da1ef4 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -4,8 +4,8 @@ Reads vehicle status from BMW connected drive portal. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.bmw_connected_drive/ """ -import logging import asyncio +import logging from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN from homeassistant.helpers.entity import Entity @@ -51,7 +51,7 @@ class BMWConnectedDriveSensor(Entity): self._attribute = attribute self._state = None self._unit_of_measurement = None - self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._sensor_name = sensor_name self._icon = icon @@ -88,19 +88,19 @@ class BMWConnectedDriveSensor(Entity): def device_state_attributes(self): """Return the state attributes of the binary sensor.""" return { - 'car': self._vehicle.modelName + 'car': self._vehicle.name } def update(self) -> None: """Read new state data from the library.""" - _LOGGER.debug('Updating %s', self._vehicle.modelName) + _LOGGER.debug('Updating %s', self._vehicle.name) vehicle_state = self._vehicle.state self._state = getattr(vehicle_state, self._attribute) if self._attribute in LENGTH_ATTRIBUTES: - self._unit_of_measurement = vehicle_state.unit_of_length + self._unit_of_measurement = 'km' elif self._attribute == 'remaining_fuel': - self._unit_of_measurement = vehicle_state.unit_of_volume + self._unit_of_measurement = 'l' else: self._unit_of_measurement = None diff --git a/requirements_all.txt b/requirements_all.txt index 52833969872..8e284b3d2a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -137,7 +137,7 @@ beautifulsoup4==4.6.0 bellows==0.5.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.4.1 +bimmer_connected==0.5.0 # homeassistant.components.blink blinkpy==0.6.0 From df35159cb4a80adafee05821077d5e908f23587c Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Sat, 24 Mar 2018 16:33:49 -0400 Subject: [PATCH 006/136] Add code owner for Manual Alarm with MQTT Control (#13438) --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index d8ebc3cff56..b7f84cf02f5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -43,6 +43,7 @@ homeassistant/components/hassio.py @home-assistant/hassio # Individual components homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt +homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell homeassistant/components/binary_sensor/hikvision.py @mezz64 homeassistant/components/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/camera/yi.py @bachya From 11930d5f202a3ab409489a216da8e2690363448c Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Sat, 24 Mar 2018 17:13:12 -0400 Subject: [PATCH 007/136] QNAP updates (#13435) * Add @colinodell to CODEOWNERS for qnap sensor * Bump qnapstats library to 0.2.5 This release adds better error handling for sharenames with no folder --- CODEOWNERS | 1 + homeassistant/components/sensor/qnap.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b7f84cf02f5..9528e7a09e9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -70,6 +70,7 @@ homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel homeassistant/components/sensor/pollen.py @bachya +homeassistant/components/sensor/qnap.py @colinodell homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/sql.py @dgomes homeassistant/components/sensor/tibber.py @danielhiversen diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index 09c9938f1c1..629a5f6a0ee 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['qnapstats==0.2.4'] +REQUIREMENTS = ['qnapstats==0.2.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8e284b3d2a9..fac18e23667 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1054,7 +1054,7 @@ pyxeoma==1.4.0 pyzabbix==0.7.4 # homeassistant.components.sensor.qnap -qnapstats==0.2.4 +qnapstats==0.2.5 # homeassistant.components.switch.rachio rachiopy==0.1.2 From e36f27d6fd5b0655375bb90c66cbe108fe967269 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 24 Mar 2018 23:04:43 +0100 Subject: [PATCH 008/136] Xiaomi MiIO Fan: Xiaomi Air Humidifier integration (#12627) * Device support for the Xiaomi Air Humidifier. * Requirements updated. * "continuation line under-indented for visual indent" fixed. * Make hound happy. * Inadvertently added light.xiaomi_miio component removed from PR. * Service descriptions added. * One of the pylint errors fixed. * Redundancy removed. * pylint: disable=no-self-use added. The method signature is important here. * Pylint fixed. * Use a unique data key per domain. * Review incorporated. * Map of available attributes added. * Pylint fixed. Attribute "volume" added. * Don't use the support flag bit mask as model identifier. Determine support features and attributes at the constructor. Use starred expressions at dicts instead of copies. * Blank line removed. * Use Async / await syntax. * Make hound happy. * Xiaomi Air Humidifier CA support added. * Duplicate method removed. * Air Purifier V3 support added. * Don't abuse the system property supported_features anymore. * python-miio version bumped. * Clean-up. * Additional supported features refactoring completed. * Additional supported features renamed properly. * Unique id added. * Device unavailable handling improved. * Refactoring. * Missed const updated. * Incomplete Air Humidifier CA support fixed. * Review incorporated * The Air Humidifier CA supports the operation mode "auto" - the standard version doesn't * Attributes are part of the common set already * Revert "Attributes are part of the common set already" This reverts commit 40b443eba0e2fc55075479fd540f977fbf4b704a. * Comment added * Service description of the set_dry_{on,off} service added * Typo fixed --- homeassistant/components/fan/services.yaml | 111 ++- homeassistant/components/fan/xiaomi_miio.py | 741 ++++++++++++++++---- 2 files changed, 691 insertions(+), 161 deletions(-) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index a306cf7767c..a74f67b83fb 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -68,50 +68,50 @@ xiaomi_miio_set_buzzer_on: description: Turn the buzzer on. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_buzzer_off: description: Turn the buzzer off. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_led_on: description: Turn the led on. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_led_off: description: Turn the led off. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_child_lock_on: description: Turn the child lock on. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_child_lock_off: description: Turn the child lock off. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_favorite_level: description: Set the favorite level. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' level: description: Level, between 0 and 16. example: 1 @@ -120,8 +120,87 @@ xiaomi_miio_set_led_brightness: description: Set the led brightness. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' brightness: description: Brightness (0 = Bright, 1 = Dim, 2 = Off) example: 1 + +xiaomi_miio_set_auto_detect_on: + description: Turn the auto detect on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_auto_detect_off: + description: Turn the auto detect off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_learn_mode_on: + description: Turn the learn mode on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_learn_mode_off: + description: Turn the learn mode off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_volume: + description: Set the sound volume. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + volume: + description: Volume, between 0 and 100. + example: 50 + +xiaomi_miio_reset_filter: + description: Reset the filter lifetime and usage. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_extra_features: + description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + features: + description: Integer, known values are 0 (default) and 1 (turbo mode). + example: 1 + +xiaomi_miio_set_target_humidity: + description: Set the target humidity. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + humidity: + description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80. + example: 50 + +xiaomi_miio_set_dry_on: + description: Turn the dry mode on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_dry_off: + description: Turn the dry mode off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 09df55200a2..a1cb0431381 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -1,16 +1,16 @@ """ -Support for Xiaomi Mi Air Purifier 2. +Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier. For more details about this platform, please refer to the documentation https://home-assistant.io/components/fan.xiaomi_miio/ """ import asyncio +from enum import Enum from functools import partial import logging import voluptuous as vol -from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA, SUPPORT_SET_SPEED, DOMAIN, ) from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, @@ -20,17 +20,40 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Xiaomi Air Purifier' -PLATFORM = 'xiaomi_miio' +DEFAULT_NAME = 'Xiaomi Miio Device' +DATA_KEY = 'fan.xiaomi_miio' + +CONF_MODEL = 'model' +MODEL_AIRPURIFIER_PRO = 'zhimi.airpurifier.v6' +MODEL_AIRPURIFIER_V3 = 'zhimi.airpurifier.v3' +MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1' +MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MODEL): vol.In( + ['zhimi.airpurifier.m1', + 'zhimi.airpurifier.m2', + 'zhimi.airpurifier.ma1', + 'zhimi.airpurifier.ma2', + 'zhimi.airpurifier.sa1', + 'zhimi.airpurifier.sa2', + 'zhimi.airpurifier.v1', + 'zhimi.airpurifier.v2', + 'zhimi.airpurifier.v3', + 'zhimi.airpurifier.v5', + 'zhimi.airpurifier.v6', + 'zhimi.humidifier.v1', + 'zhimi.humidifier.ca1']), }) REQUIREMENTS = ['python-miio==0.3.8'] +ATTR_MODEL = 'model' + +# Air Purifier ATTR_TEMPERATURE = 'temperature' ATTR_HUMIDITY = 'humidity' ATTR_AIR_QUALITY_INDEX = 'aqi' @@ -45,20 +68,190 @@ ATTR_LED_BRIGHTNESS = 'led_brightness' ATTR_MOTOR_SPEED = 'motor_speed' ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi' ATTR_PURIFY_VOLUME = 'purify_volume' - ATTR_BRIGHTNESS = 'brightness' ATTR_LEVEL = 'level' +ATTR_MOTOR2_SPEED = 'motor2_speed' +ATTR_ILLUMINANCE = 'illuminance' +ATTR_FILTER_RFID_PRODUCT_ID = 'filter_rfid_product_id' +ATTR_FILTER_RFID_TAG = 'filter_rfid_tag' +ATTR_FILTER_TYPE = 'filter_type' +ATTR_LEARN_MODE = 'learn_mode' +ATTR_SLEEP_TIME = 'sleep_time' +ATTR_SLEEP_LEARN_COUNT = 'sleep_mode_learn_count' +ATTR_EXTRA_FEATURES = 'extra_features' +ATTR_FEATURES = 'features' +ATTR_TURBO_MODE_SUPPORTED = 'turbo_mode_supported' +ATTR_AUTO_DETECT = 'auto_detect' +ATTR_SLEEP_MODE = 'sleep_mode' +ATTR_VOLUME = 'volume' +ATTR_USE_TIME = 'use_time' +ATTR_BUTTON_PRESSED = 'button_pressed' + +# Air Humidifier +ATTR_TARGET_HUMIDITY = 'target_humidity' +ATTR_TRANS_LEVEL = 'trans_level' +ATTR_HARDWARE_VERSION = 'hardware_version' + +# Air Humidifier CA +ATTR_SPEED = 'speed' +ATTR_DEPTH = 'depth' +ATTR_DRY = 'dry' + +# Map attributes to properties of the state object +AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { + ATTR_TEMPERATURE: 'temperature', + ATTR_HUMIDITY: 'humidity', + ATTR_AIR_QUALITY_INDEX: 'aqi', + ATTR_MODE: 'mode', + ATTR_FILTER_HOURS_USED: 'filter_hours_used', + ATTR_FILTER_LIFE: 'filter_life_remaining', + ATTR_FAVORITE_LEVEL: 'favorite_level', + ATTR_CHILD_LOCK: 'child_lock', + ATTR_LED: 'led', + ATTR_MOTOR_SPEED: 'motor_speed', + ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi', + ATTR_PURIFY_VOLUME: 'purify_volume', + ATTR_LEARN_MODE: 'learn_mode', + ATTR_SLEEP_TIME: 'sleep_time', + ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count', + ATTR_EXTRA_FEATURES: 'extra_features', + ATTR_TURBO_MODE_SUPPORTED: 'turbo_mode_supported', + ATTR_AUTO_DETECT: 'auto_detect', + ATTR_USE_TIME: 'use_time', + ATTR_BUTTON_PRESSED: 'button_pressed', +} + +AVAILABLE_ATTRIBUTES_AIRPURIFIER = { + **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, + ATTR_BUZZER: 'buzzer', + ATTR_LED_BRIGHTNESS: 'led_brightness', + ATTR_SLEEP_MODE: 'sleep_mode', +} + +AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = { + **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, + ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id', + ATTR_FILTER_RFID_TAG: 'filter_rfid_tag', + ATTR_FILTER_TYPE: 'filter_type', + ATTR_ILLUMINANCE: 'illuminance', + ATTR_MOTOR2_SPEED: 'motor2_speed', + ATTR_VOLUME: 'volume', +} + +AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = { + # Common set isn't used here. It's a very basic version of the device. + ATTR_AIR_QUALITY_INDEX: 'aqi', + ATTR_MODE: 'mode', + ATTR_LED: 'led', + ATTR_BUZZER: 'buzzer', + ATTR_CHILD_LOCK: 'child_lock', + ATTR_ILLUMINANCE: 'illuminance', + ATTR_FILTER_HOURS_USED: 'filter_hours_used', + ATTR_FILTER_LIFE: 'filter_life_remaining', + ATTR_MOTOR_SPEED: 'motor_speed', + # perhaps supported but unconfirmed + ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi', + ATTR_VOLUME: 'volume', + ATTR_MOTOR2_SPEED: 'motor2_speed', + ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id', + ATTR_FILTER_RFID_TAG: 'filter_rfid_tag', + ATTR_FILTER_TYPE: 'filter_type', + ATTR_PURIFY_VOLUME: 'purify_volume', + ATTR_LEARN_MODE: 'learn_mode', + ATTR_SLEEP_TIME: 'sleep_time', + ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count', + ATTR_EXTRA_FEATURES: 'extra_features', + ATTR_AUTO_DETECT: 'auto_detect', + ATTR_USE_TIME: 'use_time', + ATTR_BUTTON_PRESSED: 'button_pressed', +} + +AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = { + ATTR_TEMPERATURE: 'temperature', + ATTR_HUMIDITY: 'humidity', + ATTR_MODE: 'mode', + ATTR_BUZZER: 'buzzer', + ATTR_CHILD_LOCK: 'child_lock', + ATTR_TRANS_LEVEL: 'trans_level', + ATTR_TARGET_HUMIDITY: 'target_humidity', + ATTR_LED_BRIGHTNESS: 'led_brightness', + ATTR_BUTTON_PRESSED: 'button_pressed', + ATTR_USE_TIME: 'use_time', + ATTR_HARDWARE_VERSION: 'hardware_version', +} + +AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = { + **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER, + ATTR_SPEED: 'speed', + ATTR_DEPTH: 'depth', + ATTR_DRY: 'dry', +} + +OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle'] +OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite'] +OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle', + 'Medium', 'High', 'Strong'] SUCCESS = ['ok'] +FEATURE_SET_BUZZER = 1 +FEATURE_SET_LED = 2 +FEATURE_SET_CHILD_LOCK = 4 +FEATURE_SET_LED_BRIGHTNESS = 8 +FEATURE_SET_FAVORITE_LEVEL = 16 +FEATURE_SET_AUTO_DETECT = 32 +FEATURE_SET_LEARN_MODE = 64 +FEATURE_SET_VOLUME = 128 +FEATURE_RESET_FILTER = 256 +FEATURE_SET_EXTRA_FEATURES = 512 +FEATURE_SET_TARGET_HUMIDITY = 1024 +FEATURE_SET_DRY = 2048 + +FEATURE_FLAGS_GENERIC = (FEATURE_SET_BUZZER | + FEATURE_SET_CHILD_LOCK) + +FEATURE_FLAGS_AIRPURIFIER = (FEATURE_FLAGS_GENERIC | + FEATURE_SET_LED | + FEATURE_SET_LED_BRIGHTNESS | + FEATURE_SET_FAVORITE_LEVEL | + FEATURE_SET_LEARN_MODE | + FEATURE_RESET_FILTER | + FEATURE_SET_EXTRA_FEATURES) + +FEATURE_FLAGS_AIRPURIFIER_PRO = (FEATURE_SET_CHILD_LOCK | + FEATURE_SET_LED | + FEATURE_SET_FAVORITE_LEVEL | + FEATURE_SET_AUTO_DETECT | + FEATURE_SET_VOLUME) + +FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_FLAGS_GENERIC | + FEATURE_SET_LED) + +FEATURE_FLAGS_AIRHUMIDIFIER = (FEATURE_FLAGS_GENERIC | + FEATURE_SET_LED_BRIGHTNESS | + FEATURE_SET_TARGET_HUMIDITY) + +FEATURE_FLAGS_AIRHUMIDIFIER_CA = (FEATURE_FLAGS_AIRHUMIDIFIER | + FEATURE_SET_DRY) + SERVICE_SET_BUZZER_ON = 'xiaomi_miio_set_buzzer_on' SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off' SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on' SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off' SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on' SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off' -SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level' SERVICE_SET_LED_BRIGHTNESS = 'xiaomi_miio_set_led_brightness' +SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level' +SERVICE_SET_AUTO_DETECT_ON = 'xiaomi_miio_set_auto_detect_on' +SERVICE_SET_AUTO_DETECT_OFF = 'xiaomi_miio_set_auto_detect_off' +SERVICE_SET_LEARN_MODE_ON = 'xiaomi_miio_set_learn_mode_on' +SERVICE_SET_LEARN_MODE_OFF = 'xiaomi_miio_set_learn_mode_off' +SERVICE_SET_VOLUME = 'xiaomi_miio_set_volume' +SERVICE_RESET_FILTER = 'xiaomi_miio_reset_filter' +SERVICE_SET_EXTRA_FEATURES = 'xiaomi_miio_set_extra_features' +SERVICE_SET_TARGET_HUMIDITY = 'xiaomi_miio_set_target_humidity' +SERVICE_SET_DRY_ON = 'xiaomi_miio_set_dry_on' +SERVICE_SET_DRY_OFF = 'xiaomi_miio_set_dry_off' AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, @@ -74,6 +267,21 @@ SERVICE_SCHEMA_FAVORITE_LEVEL = AIRPURIFIER_SERVICE_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Clamp(min=0, max=16)) }) +SERVICE_SCHEMA_VOLUME = AIRPURIFIER_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_VOLUME): + vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100)) +}) + +SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_FEATURES): + vol.All(vol.Coerce(int), vol.Range(min=0)) +}) + +SERVICE_SCHEMA_TARGET_HUMIDITY = AIRPURIFIER_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_HUMIDITY): + vol.All(vol.Coerce(int), vol.In([30, 40, 50, 60, 70, 80])) +}) + SERVICE_TO_METHOD = { SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'}, SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'}, @@ -81,59 +289,99 @@ SERVICE_TO_METHOD = { SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'}, SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'}, SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'}, - SERVICE_SET_FAVORITE_LEVEL: { - 'method': 'async_set_favorite_level', - 'schema': SERVICE_SCHEMA_FAVORITE_LEVEL}, + SERVICE_SET_AUTO_DETECT_ON: {'method': 'async_set_auto_detect_on'}, + SERVICE_SET_AUTO_DETECT_OFF: {'method': 'async_set_auto_detect_off'}, + SERVICE_SET_LEARN_MODE_ON: {'method': 'async_set_learn_mode_on'}, + SERVICE_SET_LEARN_MODE_OFF: {'method': 'async_set_learn_mode_off'}, + SERVICE_RESET_FILTER: {'method': 'async_reset_filter'}, SERVICE_SET_LED_BRIGHTNESS: { 'method': 'async_set_led_brightness', 'schema': SERVICE_SCHEMA_LED_BRIGHTNESS}, + SERVICE_SET_FAVORITE_LEVEL: { + 'method': 'async_set_favorite_level', + 'schema': SERVICE_SCHEMA_FAVORITE_LEVEL}, + SERVICE_SET_VOLUME: { + 'method': 'async_set_volume', + 'schema': SERVICE_SCHEMA_VOLUME}, + SERVICE_SET_EXTRA_FEATURES: { + 'method': 'async_set_extra_features', + 'schema': SERVICE_SCHEMA_EXTRA_FEATURES}, + SERVICE_SET_TARGET_HUMIDITY: { + 'method': 'async_set_target_humidity', + 'schema': SERVICE_SCHEMA_TARGET_HUMIDITY}, + SERVICE_SET_DRY_ON: {'method': 'async_set_dry_on'}, + SERVICE_SET_DRY_OFF: {'method': 'async_set_dry_off'}, } # pylint: disable=unused-argument -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the air purifier from config.""" - from miio import AirPurifier, DeviceException - if PLATFORM not in hass.data: - hass.data[PLATFORM] = {} +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the miio fan device from config.""" + from miio import Device, DeviceException + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} host = config.get(CONF_HOST) name = config.get(CONF_NAME) token = config.get(CONF_TOKEN) + model = config.get(CONF_MODEL) _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + unique_id = None - try: + if model is None: + try: + miio_device = Device(host, token) + device_info = miio_device.info() + model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) + _LOGGER.info("%s %s %s detected", + model, + device_info.firmware_version, + device_info.hardware_version) + except DeviceException: + raise PlatformNotReady + + if model.startswith('zhimi.airpurifier.'): + from miio import AirPurifier air_purifier = AirPurifier(host, token) + device = XiaomiAirPurifier(name, air_purifier, model, unique_id) + elif model.startswith('zhimi.humidifier.'): + from miio import AirHumidifier + air_humidifier = AirHumidifier(host, token) + device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id) + else: + _LOGGER.error( + 'Unsupported device found! Please create an issue at ' + 'https://github.com/syssi/xiaomi_airpurifier/issues ' + 'and provide the following data: %s', model) + return False - xiaomi_air_purifier = XiaomiAirPurifier(name, air_purifier) - hass.data[PLATFORM][host] = xiaomi_air_purifier - except DeviceException: - raise PlatformNotReady + hass.data[DATA_KEY][host] = device + async_add_devices([device], update_before_add=True) - async_add_devices([xiaomi_air_purifier], update_before_add=True) - - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on XiaomiAirPurifier.""" method = SERVICE_TO_METHOD.get(service.service) params = {key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID} entity_ids = service.data.get(ATTR_ENTITY_ID) if entity_ids: - devices = [device for device in hass.data[PLATFORM].values() if + devices = [device for device in hass.data[DATA_KEY].values() if device.entity_id in entity_ids] else: - devices = hass.data[PLATFORM].values() + devices = hass.data[DATA_KEY].values() update_tasks = [] for device in devices: - yield from getattr(device, method['method'])(**params) + if not hasattr(device, method['method']): + continue + await getattr(device, method['method'])(**params) update_tasks.append(device.async_update_ha_state(True)) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) for air_purifier_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[air_purifier_service].get( @@ -142,31 +390,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): DOMAIN, air_purifier_service, async_service_handler, schema=schema) -class XiaomiAirPurifier(FanEntity): - """Representation of a Xiaomi Air Purifier.""" +class XiaomiGenericDevice(FanEntity): + """Representation of a generic Xiaomi device.""" - def __init__(self, name, air_purifier): - """Initialize the air purifier.""" + def __init__(self, name, device, model, unique_id): + """Initialize the generic Xiaomi device.""" self._name = name + self._device = device + self._model = model + self._unique_id = unique_id - self._air_purifier = air_purifier + self._available = False self._state = None self._state_attrs = { - ATTR_AIR_QUALITY_INDEX: None, - ATTR_TEMPERATURE: None, - ATTR_HUMIDITY: None, - ATTR_MODE: None, - ATTR_FILTER_HOURS_USED: None, - ATTR_FILTER_LIFE: None, - ATTR_FAVORITE_LEVEL: None, - ATTR_BUZZER: None, - ATTR_CHILD_LOCK: None, - ATTR_LED: None, - ATTR_LED_BRIGHTNESS: None, - ATTR_MOTOR_SPEED: None, - ATTR_AVERAGE_AIR_QUALITY_INDEX: None, - ATTR_PURIFY_VOLUME: None, + ATTR_MODEL: self._model, } + self._device_features = FEATURE_FLAGS_GENERIC self._skip_update = False @property @@ -176,9 +415,14 @@ class XiaomiAirPurifier(FanEntity): @property def should_poll(self): - """Poll the fan.""" + """Poll the device.""" return True + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + @property def name(self): """Return the name of the device if any.""" @@ -187,7 +431,7 @@ class XiaomiAirPurifier(FanEntity): @property def available(self): """Return true when state is known.""" - return self._state is not None + return self._available @property def device_state_attributes(self): @@ -196,50 +440,116 @@ class XiaomiAirPurifier(FanEntity): @property def is_on(self): - """Return true if fan is on.""" + """Return true if device is on.""" return self._state - @asyncio.coroutine - def _try_command(self, mask_error, func, *args, **kwargs): - """Call an air purifier command handling error messages.""" + @staticmethod + def _extract_value_from_attribute(state, attribute): + value = getattr(state, attribute) + if isinstance(value, Enum): + return value.value + + return value + + async def _try_command(self, mask_error, func, *args, **kwargs): + """Call a miio device command handling error messages.""" from miio import DeviceException try: - result = yield from self.hass.async_add_job( + result = await self.hass.async_add_job( partial(func, *args, **kwargs)) - _LOGGER.debug("Response received from air purifier: %s", result) + _LOGGER.debug("Response received from miio device: %s", result) return result == SUCCESS except DeviceException as exc: _LOGGER.error(mask_error, exc) + self._available = False return False - @asyncio.coroutine - def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None: - """Turn the fan on.""" + async def async_turn_on(self, speed: str = None, + **kwargs) -> None: + """Turn the device on.""" if speed: # If operation mode was set the device must not be turned on. - result = yield from self.async_set_speed(speed) + result = await self.async_set_speed(speed) else: - result = yield from self._try_command( - "Turning the air purifier on failed.", self._air_purifier.on) + result = await self._try_command( + "Turning the miio device on failed.", self._device.on) if result: self._state = True self._skip_update = True - @asyncio.coroutine - def async_turn_off(self: ToggleEntity, **kwargs) -> None: - """Turn the fan off.""" - result = yield from self._try_command( - "Turning the air purifier off failed.", self._air_purifier.off) + async def async_turn_off(self, **kwargs) -> None: + """Turn the device off.""" + result = await self._try_command( + "Turning the miio device off failed.", self._device.off) if result: self._state = False self._skip_update = True - @asyncio.coroutine - def async_update(self): + async def async_set_buzzer_on(self): + """Turn the buzzer on.""" + if self._device_features & FEATURE_SET_BUZZER == 0: + return + + await self._try_command( + "Turning the buzzer of the miio device on failed.", + self._device.set_buzzer, True) + + async def async_set_buzzer_off(self): + """Turn the buzzer off.""" + if self._device_features & FEATURE_SET_BUZZER == 0: + return + + await self._try_command( + "Turning the buzzer of the miio device off failed.", + self._device.set_buzzer, False) + + async def async_set_child_lock_on(self): + """Turn the child lock on.""" + if self._device_features & FEATURE_SET_CHILD_LOCK == 0: + return + + await self._try_command( + "Turning the child lock of the miio device on failed.", + self._device.set_child_lock, True) + + async def async_set_child_lock_off(self): + """Turn the child lock off.""" + if self._device_features & FEATURE_SET_CHILD_LOCK == 0: + return + + await self._try_command( + "Turning the child lock of the miio device off failed.", + self._device.set_child_lock, False) + + +class XiaomiAirPurifier(XiaomiGenericDevice): + """Representation of a Xiaomi Air Purifier.""" + + def __init__(self, name, device, model, unique_id): + """Initialize the plug switch.""" + super().__init__(name, device, model, unique_id) + + if self._model == MODEL_AIRPURIFIER_PRO: + self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO + self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO + elif self._model == MODEL_AIRPURIFIER_V3: + self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3 + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 + self._speed_list = OPERATION_MODES_AIRPURIFIER_V3 + else: + self._device_features = FEATURE_FLAGS_AIRPURIFIER + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER + self._speed_list = OPERATION_MODES_AIRPURIFIER + + self._state_attrs.update( + {attribute: None for attribute in self._available_attributes}) + + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException @@ -249,40 +559,24 @@ class XiaomiAirPurifier(FanEntity): return try: - state = yield from self.hass.async_add_job( - self._air_purifier.status) + state = await self.hass.async_add_job( + self._device.status) _LOGGER.debug("Got new state: %s", state) + self._available = True self._state = state.is_on - self._state_attrs = { - ATTR_TEMPERATURE: state.temperature, - ATTR_HUMIDITY: state.humidity, - ATTR_AIR_QUALITY_INDEX: state.aqi, - ATTR_MODE: state.mode.value, - ATTR_FILTER_HOURS_USED: state.filter_hours_used, - ATTR_FILTER_LIFE: state.filter_life_remaining, - ATTR_FAVORITE_LEVEL: state.favorite_level, - ATTR_BUZZER: state.buzzer, - ATTR_CHILD_LOCK: state.child_lock, - ATTR_LED: state.led, - ATTR_MOTOR_SPEED: state.motor_speed, - ATTR_AVERAGE_AIR_QUALITY_INDEX: state.average_aqi, - ATTR_PURIFY_VOLUME: state.purify_volume, - } - - if state.led_brightness: - self._state_attrs[ - ATTR_LED_BRIGHTNESS] = state.led_brightness.value + self._state_attrs.update( + {key: self._extract_value_from_attribute(state, value) for + key, value in self._available_attributes.items()}) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) @property - def speed_list(self: ToggleEntity) -> list: + def speed_list(self) -> list: """Get the list of available speeds.""" - from miio.airpurifier import OperationMode - return [mode.name for mode in OperationMode] + return self._speed_list @property def speed(self): @@ -294,70 +588,227 @@ class XiaomiAirPurifier(FanEntity): return None - @asyncio.coroutine - def async_set_speed(self: ToggleEntity, speed: str) -> None: + async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" - _LOGGER.debug("Setting the operation mode to: %s", speed) + if self.supported_features & SUPPORT_SET_SPEED == 0: + return + from miio.airpurifier import OperationMode - yield from self._try_command( - "Setting operation mode of the air purifier failed.", - self._air_purifier.set_mode, OperationMode[speed.title()]) + _LOGGER.debug("Setting the operation mode to: %s", speed) - @asyncio.coroutine - def async_set_buzzer_on(self): - """Turn the buzzer on.""" - yield from self._try_command( - "Turning the buzzer of the air purifier on failed.", - self._air_purifier.set_buzzer, True) + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, OperationMode[speed.title()]) - @asyncio.coroutine - def async_set_buzzer_off(self): - """Turn the buzzer off.""" - yield from self._try_command( - "Turning the buzzer of the air purifier off failed.", - self._air_purifier.set_buzzer, False) - - @asyncio.coroutine - def async_set_led_on(self): + async def async_set_led_on(self): """Turn the led on.""" - yield from self._try_command( - "Turning the led of the air purifier off failed.", - self._air_purifier.set_led, True) + if self._device_features & FEATURE_SET_LED == 0: + return - @asyncio.coroutine - def async_set_led_off(self): + await self._try_command( + "Turning the led of the miio device off failed.", + self._device.set_led, True) + + async def async_set_led_off(self): """Turn the led off.""" - yield from self._try_command( - "Turning the led of the air purifier off failed.", - self._air_purifier.set_led, False) + if self._device_features & FEATURE_SET_LED == 0: + return - @asyncio.coroutine - def async_set_child_lock_on(self): - """Turn the child lock on.""" - yield from self._try_command( - "Turning the child lock of the air purifier on failed.", - self._air_purifier.set_child_lock, True) + await self._try_command( + "Turning the led of the miio device off failed.", + self._device.set_led, False) - @asyncio.coroutine - def async_set_child_lock_off(self): - """Turn the child lock off.""" - yield from self._try_command( - "Turning the child lock of the air purifier off failed.", - self._air_purifier.set_child_lock, False) - - @asyncio.coroutine - def async_set_led_brightness(self, brightness: int = 2): + async def async_set_led_brightness(self, brightness: int = 2): """Set the led brightness.""" + if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: + return + from miio.airpurifier import LedBrightness - yield from self._try_command( - "Setting the led brightness of the air purifier failed.", - self._air_purifier.set_led_brightness, LedBrightness(brightness)) + await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, LedBrightness(brightness)) - @asyncio.coroutine - def async_set_favorite_level(self, level: int = 1): + async def async_set_favorite_level(self, level: int = 1): """Set the favorite level.""" - yield from self._try_command( - "Setting the favorite level of the air purifier failed.", - self._air_purifier.set_favorite_level, level) + if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0: + return + + await self._try_command( + "Setting the favorite level of the miio device failed.", + self._device.set_favorite_level, level) + + async def async_set_auto_detect_on(self): + """Turn the auto detect on.""" + if self._device_features & FEATURE_SET_AUTO_DETECT == 0: + return + + await self._try_command( + "Turning the auto detect of the miio device on failed.", + self._device.set_auto_detect, True) + + async def async_set_auto_detect_off(self): + """Turn the auto detect off.""" + if self._device_features & FEATURE_SET_AUTO_DETECT == 0: + return + + await self._try_command( + "Turning the auto detect of the miio device off failed.", + self._device.set_auto_detect, False) + + async def async_set_learn_mode_on(self): + """Turn the learn mode on.""" + if self._device_features & FEATURE_SET_LEARN_MODE == 0: + return + + await self._try_command( + "Turning the learn mode of the miio device on failed.", + self._device.set_learn_mode, True) + + async def async_set_learn_mode_off(self): + """Turn the learn mode off.""" + if self._device_features & FEATURE_SET_LEARN_MODE == 0: + return + + await self._try_command( + "Turning the learn mode of the miio device off failed.", + self._device.set_learn_mode, False) + + async def async_set_volume(self, volume: int = 50): + """Set the sound volume.""" + if self._device_features & FEATURE_SET_VOLUME == 0: + return + + await self._try_command( + "Setting the sound volume of the miio device failed.", + self._device.set_volume, volume) + + async def async_set_extra_features(self, features: int = 1): + """Set the extra features.""" + if self._device_features & FEATURE_SET_EXTRA_FEATURES == 0: + return + + await self._try_command( + "Setting the extra features of the miio device failed.", + self._device.set_extra_features, features) + + async def async_reset_filter(self): + """Reset the filter lifetime and usage.""" + if self._device_features & FEATURE_RESET_FILTER == 0: + return + + await self._try_command( + "Resetting the filter lifetime of the miio device failed.", + self._device.reset_filter) + + +class XiaomiAirHumidifier(XiaomiGenericDevice): + """Representation of a Xiaomi Air Humidifier.""" + + def __init__(self, name, device, model, unique_id): + """Initialize the plug switch.""" + from miio.airpurifier import OperationMode + + super().__init__(name, device, model, unique_id) + + if self._model == MODEL_AIRHUMIDIFIER_CA: + self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA + self._speed_list = [mode.name for mode in OperationMode] + else: + self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER + self._speed_list = [mode.name for mode in OperationMode if + mode.name != 'Auto'] + + self._state_attrs.update( + {attribute: None for attribute in self._available_attributes}) + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + + # On state change the device doesn't provide the new state immediately. + if self._skip_update: + self._skip_update = False + return + + try: + state = await self.hass.async_add_job(self._device.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True + self._state = state.is_on + self._state_attrs.update( + {key: self._extract_value_from_attribute(state, value) for + key, value in self._available_attributes.items()}) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + def speed_list(self) -> list: + """Get the list of available speeds.""" + return self._speed_list + + @property + def speed(self): + """Return the current speed.""" + if self._state: + from miio.airhumidifier import OperationMode + + return OperationMode(self._state_attrs[ATTR_MODE]).name + + return None + + async def async_set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + if self.supported_features & SUPPORT_SET_SPEED == 0: + return + + from miio.airhumidifier import OperationMode + + _LOGGER.debug("Setting the operation mode to: %s", speed) + + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, OperationMode[speed.title()]) + + async def async_set_led_brightness(self, brightness: int = 2): + """Set the led brightness.""" + if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: + return + + from miio.airhumidifier import LedBrightness + + await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, LedBrightness(brightness)) + + async def async_set_target_humidity(self, humidity: int = 40): + """Set the target humidity.""" + if self._device_features & FEATURE_SET_TARGET_HUMIDITY == 0: + return + + await self._try_command( + "Setting the target humidity of the miio device failed.", + self._device.set_target_humidity, humidity) + + async def async_set_dry_on(self): + """Turn the dry mode on.""" + if self._device_features & FEATURE_SET_DRY == 0: + return + + await self._try_command( + "Turning the dry mode of the miio device off failed.", + self._device.set_dry, True) + + async def async_set_dry_off(self): + """Turn the dry mode off.""" + if self._device_features & FEATURE_SET_DRY == 0: + return + + await self._try_command( + "Turning the dry mode of the miio device off failed.", + self._device.set_dry, False) From 7166d53e2b0516ce2263562a56de51eb0420b430 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 25 Mar 2018 01:12:26 +0100 Subject: [PATCH 009/136] Log invalid templates in script delays (#13423) * Log invalid templates in script delays * Abort on error * Remove unused import --- homeassistant/helpers/script.py | 15 ++++++++++----- tests/helpers/test_script.py | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index abfdde8c8e1..f2ae36e7fd0 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -97,11 +97,16 @@ class Script(): delay = action[CONF_DELAY] - if isinstance(delay, template.Template): - delay = vol.All( - cv.time_period, - cv.positive_timedelta)( - delay.async_render(variables)) + try: + if isinstance(delay, template.Template): + delay = vol.All( + cv.time_period, + cv.positive_timedelta)( + delay.async_render(variables)) + except (TemplateError, vol.Invalid) as ex: + _LOGGER.error("Error rendering '%s' delay template: %s", + self.name, ex) + break unsub = async_track_point_in_utc_time( self.hass, async_script_delay, diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index a8ae20ad69b..4297ca26e7d 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -218,6 +218,32 @@ class TestScriptHelper(unittest.TestCase): assert not script_obj.is_running assert len(events) == 2 + def test_delay_invalid_template(self): + """Test the delay as a template that fails.""" + event = 'test_event' + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + self.hass.bus.listen(event, record_event) + + script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'delay': '{{ invalid_delay }}'}, + {'delay': {'seconds': 5}}, + {'event': event}])) + + with mock.patch.object(script, '_LOGGER') as mock_logger: + script_obj.run() + self.hass.block_till_done() + assert mock_logger.error.called + + assert not script_obj.is_running + assert len(events) == 1 + def test_cancel_while_delay(self): """Test the cancelling while the delay is present.""" event = 'test_event' From f3ccbda05435d1e0e4ae873b03077f7d47b00e09 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 25 Mar 2018 08:24:03 +0200 Subject: [PATCH 010/136] Bump songpal version, fixes lots of issues mentioned in #13022 (#13440) --- homeassistant/components/media_player/songpal.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index e43f5951db7..955456f2465 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-songpal==0.0.6'] +REQUIREMENTS = ['python-songpal==0.0.7'] SUPPORT_SONGPAL = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | \ @@ -101,7 +101,7 @@ class SongpalDevice(MediaPlayerDevice): import songpal self._name = name self.endpoint = endpoint - self.dev = songpal.Protocol(self.endpoint) + self.dev = songpal.Device(self.endpoint) self._sysinfo = None self._state = False diff --git a/requirements_all.txt b/requirements_all.txt index fac18e23667..af3fd68ec64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -976,7 +976,7 @@ python-roku==3.1.5 python-sochain-api==0.0.2 # homeassistant.components.media_player.songpal -python-songpal==0.0.6 +python-songpal==0.0.7 # homeassistant.components.sensor.synologydsm python-synology==0.1.0 From 2d2e8034d68f5e932921c8bf22080f87e0afbf8c Mon Sep 17 00:00:00 2001 From: Marc Forth Date: Sun, 25 Mar 2018 08:45:25 +0100 Subject: [PATCH 011/136] Removed the google home warning from emulated_hue (#13436) * Removed the google home warning from emulated_hue * Update test_init.py * Update test_init.py --- .../components/emulated_hue/__init__.py | 4 ---- tests/components/emulated_hue/test_init.py | 16 +--------------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 09ce1a57060..fa558cf299f 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -158,10 +158,6 @@ class Config(object): "Listen port not specified, defaulting to %s", self.listen_port) - if self.type == TYPE_GOOGLE and self.listen_port != 80: - _LOGGER.warning("When targeting Google Home, listening port has " - "to be port 80") - # Get whether or not UPNP binds to multicast address (239.255.255.250) # or to the unicast address (host_ip_addr) self.upnp_bind_multicast = conf.get( diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 06613f1336a..2f443eb5d6e 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -3,7 +3,7 @@ import json from unittest.mock import patch, Mock, mock_open -from homeassistant.components.emulated_hue import Config, _LOGGER +from homeassistant.components.emulated_hue import Config def test_config_google_home_entity_id_to_number(): @@ -112,17 +112,3 @@ def test_config_alexa_entity_id_to_number(): entity_id = conf.number_to_entity_id('light.test') assert entity_id == 'light.test' - - -def test_warning_config_google_home_listen_port(): - """Test we warn when non-default port is used for Google Home.""" - with patch.object(_LOGGER, 'warning') as mock_warn: - Config(None, { - 'type': 'google_home', - 'host_ip': '123.123.123.123', - 'listen_port': 8300 - }) - - assert mock_warn.called - assert mock_warn.mock_calls[0][1][0] == \ - "When targeting Google Home, listening port has to be port 80" From 3a765692e71f3a383c30e3e3bde54cee5b2bbd37 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 25 Mar 2018 00:46:47 -0700 Subject: [PATCH 012/136] Fixing odometer to display km (#13427) --- homeassistant/components/sensor/tesla.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/sensor/tesla.py b/homeassistant/components/sensor/tesla.py index 1ffc97bb137..3233ebb1780 100644 --- a/homeassistant/components/sensor/tesla.py +++ b/homeassistant/components/sensor/tesla.py @@ -86,6 +86,8 @@ class TeslaSensor(TeslaDevice, Entity): self._unit = LENGTH_MILES else: self._unit = LENGTH_KILOMETERS + self.current_value /= 0.621371 + self.current_value = round(self.current_value, 2) else: self.current_value = self.tesla_device.get_value() if self.tesla_device.bin_type == 0x5: @@ -95,3 +97,5 @@ class TeslaSensor(TeslaDevice, Entity): self._unit = LENGTH_MILES else: self._unit = LENGTH_KILOMETERS + self.current_value /= 0.621371 + self.current_value = round(self.current_value, 2) From 594a5b7d29b1a08b3e246f13a17907d0c7746872 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 25 Mar 2018 09:47:10 +0200 Subject: [PATCH 013/136] LimitlessLED hs_color fixes (#13425) --- homeassistant/components/light/limitlessled.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index 5a6a0a34959..bb84b3a6fed 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -197,7 +197,7 @@ class LimitlessLEDGroup(Light): self._is_on = (last_state.state == STATE_ON) self._brightness = last_state.attributes.get('brightness') self._temperature = last_state.attributes.get('color_temp') - self._color = last_state.attributes.get('rgb_color') + self._color = last_state.attributes.get('hs_color') @property def should_poll(self): @@ -336,4 +336,4 @@ class LimitlessLEDGroup(Light): def limitlessled_color(self): """Convert Home Assistant HS list to RGB Color tuple.""" from limitlessled import Color - return Color(color_hs_to_RGB(*tuple(self._color))) + return Color(*color_hs_to_RGB(*tuple(self._color))) From 55daea5169b19cb20f727444a523011bd3fc7f10 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 25 Mar 2018 12:51:11 +0200 Subject: [PATCH 014/136] Improve detection of entity names in templates (#13432) * Improve detection of entity names in templates * Only test variables --- homeassistant/helpers/template.py | 5 +++-- tests/helpers/test_template.py | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3dd65aa362c..28ab4e9bfa0 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -13,7 +13,7 @@ from jinja2.sandbox import ImmutableSandboxedEnvironment from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT, MATCH_ALL, STATE_UNKNOWN) -from homeassistant.core import State +from homeassistant.core import State, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper from homeassistant.loader import bind_hass, get_component @@ -73,7 +73,8 @@ def extract_entities(template, variables=None): extraction_final.append(result[0]) if variables and result[1] in variables and \ - isinstance(variables[result[1]], str): + isinstance(variables[result[1]], str) and \ + valid_entity_id(variables[result[1]]): extraction_final.append(variables[result[1]]) if extraction_final: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 47e46bae3c7..def06ea9284 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -836,6 +836,12 @@ is_state_attr('device_tracker.phone_2', 'battery', 40) "{{ is_state(trigger.entity_id, 'off') }}", {'trigger': {'entity_id': 'input_boolean.switch'}})) + self.assertEqual( + MATCH_ALL, + template.extract_entities( + "{{ is_state('media_player.' ~ where , 'playing') }}", + {'where': 'livingroom'})) + @asyncio.coroutine def test_state_with_unit(hass): From 7db37a38344c3ee57f16c97c402eaacfe8441417 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 25 Mar 2018 12:53:15 +0200 Subject: [PATCH 015/136] HomeKit: Bugfix & improved logging (#13431) * Bugfix & improved logging * Removed logging statements * Removed logging test --- homeassistant/components/homekit/__init__.py | 4 ---- homeassistant/components/homekit/type_covers.py | 1 + homeassistant/components/homekit/type_lights.py | 4 ++++ .../components/homekit/type_security_systems.py | 1 + homeassistant/components/homekit/type_switches.py | 1 + homeassistant/components/homekit/type_thermostats.py | 4 ++++ tests/components/homekit/test_get_accessories.py | 11 ----------- 7 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 02449607bf2..4854a828e41 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -73,8 +73,6 @@ async def async_setup(hass, config): def get_accessory(hass, state, aid, config): """Take state and return an accessory object if supported.""" - _LOGGER.debug('', - state.entity_id, aid, config) if not aid: _LOGGER.warning('The entitiy "%s" is not supported, since it ' 'generates an invalid aid, please change it.', @@ -129,8 +127,6 @@ def get_accessory(hass, state, aid, config): _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid) - _LOGGER.warning('The entity "%s" is not supported yet', - state.entity_id) return None diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 36cfa4d635a..7616ef05fdf 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -46,6 +46,7 @@ class WindowCovering(HomeAccessory): def move_cover(self, value): """Move cover to value if call came from HomeKit.""" + self.char_target_position.set_value(value, should_callback=False) if value != self.current_position: _LOGGER.debug('%s: Set position to %d', self._entity_id, value) self.homekit_target = value diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index c723fcc08a6..2415bb1a4df 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -71,6 +71,7 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self._entity_id, value) self._flag[CHAR_ON] = True + self.char_on.set_value(value, should_callback=False) if value == 1: self._hass.components.light.turn_on(self._entity_id) @@ -81,6 +82,7 @@ class Light(HomeAccessory): """Set brightness if call came from HomeKit.""" _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) self._flag[CHAR_BRIGHTNESS] = True + self.char_brightness.set_value(value, should_callback=False) self._hass.components.light.turn_on( self._entity_id, brightness_pct=value) @@ -88,6 +90,7 @@ class Light(HomeAccessory): """Set saturation if call came from HomeKit.""" _LOGGER.debug('%s: Set saturation to %d', self._entity_id, value) self._flag[CHAR_SATURATION] = True + self.char_saturation.set_value(value, should_callback=False) self._saturation = value self.set_color() @@ -95,6 +98,7 @@ class Light(HomeAccessory): """Set hue if call came from HomeKit.""" _LOGGER.debug('%s: Set hue to %d', self._entity_id, value) self._flag[CHAR_HUE] = True + self.char_hue.set_value(value, should_callback=False) self._hue = value self.set_color() diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 1d47160f9d2..146fca95b53 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -54,6 +54,7 @@ class SecuritySystem(HomeAccessory): _LOGGER.debug('%s: Set security state to %d', self._entity_id, value) self.flag_target_state = True + self.char_target_state.set_value(value, should_callback=False) hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index fd3291ffe23..1f19893d0be 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -36,6 +36,7 @@ class Switch(HomeAccessory): _LOGGER.debug('%s: Set switch state to %s', self._entity_id, value) self.flag_target_state = True + self.char_on.set_value(value, should_callback=False) service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self._hass.services.call(self._domain, service, {ATTR_ENTITY_ID: self._entity_id}) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index b73b492ba74..3f545e90eb3 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -97,6 +97,7 @@ class Thermostat(HomeAccessory): def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" + self.char_target_heat_cool.set_value(value, should_callback=False) if value in HC_HOMEKIT_TO_HASS: _LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value) self.heat_cool_flag_target_state = True @@ -109,6 +110,7 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set cooling threshold temperature to %.2f', self._entity_id, value) self.coolingthresh_flag_target_state = True + self.char_cooling_thresh_temp.set_value(value, should_callback=False) low = self.char_heating_thresh_temp.value self._hass.components.climate.set_temperature( entity_id=self._entity_id, target_temp_high=value, @@ -119,6 +121,7 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set heating threshold temperature to %.2f', self._entity_id, value) self.heatingthresh_flag_target_state = True + self.char_heating_thresh_temp.set_value(value, should_callback=False) # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value self._hass.components.climate.set_temperature( @@ -130,6 +133,7 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set target temperature to %.2f', self._entity_id, value) self.temperature_flag_target_state = True + self.char_target_temp.set_value(value, should_callback=False) self._hass.components.climate.set_temperature( temperature=value, entity_id=self._entity_id) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index e6dbe1ff729..ee7baae2755 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -16,17 +16,6 @@ _LOGGER = logging.getLogger(__name__) CONFIG = {} -def test_get_accessory_invalid(caplog): - """Test with unsupported component.""" - assert get_accessory(None, State('test.unsupported', 'on'), 2, None) \ - is None - assert caplog.records[1].levelname == 'WARNING' - - assert get_accessory(None, State('test.test', 'on'), None, None) \ - is None - assert caplog.records[3].levelname == 'WARNING' - - class TestGetAccessories(unittest.TestCase): """Methods to test the get_accessory method.""" From eaf81150eac9b10d43e89ca35dbb5972aae26526 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 25 Mar 2018 14:23:53 +0200 Subject: [PATCH 016/136] Upgrade keyring to 12.0.0 and keyrings.alt to 3.0 (#13452) --- homeassistant/scripts/keyring.py | 2 +- requirements_all.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 64ad09bcd70..82a57c90263 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,7 +5,7 @@ import os from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['keyring==11.0.0', 'keyrings.alt==2.3'] +REQUIREMENTS = ['keyring==12.0.0', 'keyrings.alt==3.0'] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index af3fd68ec64..3eb4367d7de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,10 +430,10 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.6 # homeassistant.scripts.keyring -keyring==11.0.0 +keyring==12.0.0 # homeassistant.scripts.keyring -keyrings.alt==2.3 +keyrings.alt==3.0 # homeassistant.components.device_tracker.owntracks # homeassistant.components.device_tracker.owntracks_http From b99663296592f6c8c480b02bdd4d08d7088cc291 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 25 Mar 2018 14:25:00 +0200 Subject: [PATCH 017/136] Upgrade aiohttp to 3.1.0 (#13451) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e43e1f3dafe..317c1c8bc6c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.0.9 +aiohttp==3.1.0 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 diff --git a/requirements_all.txt b/requirements_all.txt index 3eb4367d7de..79e371e0b48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.0.9 +aiohttp==3.1.0 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 diff --git a/setup.py b/setup.py index a317aeb18f1..9324713e71e 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ REQUIRES = [ 'jinja2>=2.10', 'voluptuous==0.11.1', 'typing>=3,<4', - 'aiohttp==3.0.9', + 'aiohttp==3.1.0', 'async_timeout==2.0.1', 'astral==1.6', 'certifi>=2017.4.17', From 6d20a84f0e1e0c6114129f36ade817c8b28b0262 Mon Sep 17 00:00:00 2001 From: Patrick Hofmann Date: Sun, 25 Mar 2018 23:25:28 +0200 Subject: [PATCH 018/136] Security fix & lock for HomeMatic (#11980) * HomeMatic KeyMatic device become a real lock component * Adds supported features to lock component. Locks may are capable to open the door latch. If component is support it, the SUPPORT_OPENING bitmask can be supplied in the supported_features property. * hound improvements. * Travis improvements. * Improvements from review process * Simplifies is_locked method * Adds an openable lock in the lock demo component * removes blank line * Adds test for openable demo lock and lint and reviewer improvements. * adds new line... * Comment end with a period. * Additional blank line. * Mock service based testing, lint fixes * Update description --- .../components/homematic/__init__.py | 9 ++- homeassistant/components/lock/__init__.py | 33 ++++++++++- homeassistant/components/lock/demo.py | 19 +++++- homeassistant/components/lock/homematic.py | 58 +++++++++++++++++++ tests/components/lock/test_demo.py | 12 +++- 5 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/lock/homematic.py diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 1accf038575..c542cd9e88e 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -33,6 +33,7 @@ DISCOVER_SENSORS = 'homematic.sensor' DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor' DISCOVER_COVER = 'homematic.cover' DISCOVER_CLIMATE = 'homematic.climate' +DISCOVER_LOCKS = 'homematic.locks' ATTR_DISCOVER_DEVICES = 'devices' ATTR_PARAM = 'param' @@ -59,7 +60,7 @@ SERVICE_SET_INSTALL_MODE = 'set_install_mode' HM_DEVICE_TYPES = { DISCOVER_SWITCHES: [ 'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren', - 'IPSwitchPowermeter', 'KeyMatic', 'HMWIOSwitch', 'Rain', 'EcoLogic'], + 'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic'], DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'], DISCOVER_SENSORS: [ 'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP', @@ -78,7 +79,8 @@ HM_DEVICE_TYPES = { 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'WiredSensor', 'PresenceIP'], - DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'] + DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], + DISCOVER_LOCKS: ['KeyMatic'] } HM_IGNORE_DISCOVERY_NODE = [ @@ -464,7 +466,8 @@ def _system_callback_handler(hass, config, src, *args): ('cover', DISCOVER_COVER), ('binary_sensor', DISCOVER_BINARY_SENSORS), ('sensor', DISCOVER_SENSORS), - ('climate', DISCOVER_CLIMATE)): + ('climate', DISCOVER_CLIMATE), + ('lock', DISCOVER_LOCKS)): # Get all devices of a specific type found_devices = _get_devices( hass, discovery_type, addresses, interface) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index d03bbebd696..b3e4ac8f0ff 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, - STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK) + STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN) from homeassistant.components import group ATTR_CHANGED_BY = 'changed_by' @@ -39,6 +39,9 @@ LOCK_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_CODE): cv.string, }) +# Bitfield of features supported by the lock entity +SUPPORT_OPEN = 1 + _LOGGER = logging.getLogger(__name__) PROP_TO_ATTR = { @@ -78,6 +81,18 @@ def unlock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_UNLOCK, data) +@bind_hass +def open_lock(hass, entity_id=None, code=None): + """Open all or specified locks.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_OPEN, data) + + @asyncio.coroutine def async_setup(hass, config): """Track states and offer events for locks.""" @@ -97,6 +112,8 @@ def async_setup(hass, config): for entity in target_locks: if service.service == SERVICE_LOCK: yield from entity.async_lock(code=code) + elif service.service == SERVICE_OPEN: + yield from entity.async_open(code=code) else: yield from entity.async_unlock(code=code) @@ -113,6 +130,9 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_LOCK, async_handle_lock_service, schema=LOCK_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_OPEN, async_handle_lock_service, + schema=LOCK_SERVICE_SCHEMA) return True @@ -158,6 +178,17 @@ class LockDevice(Entity): """ return self.hass.async_add_job(ft.partial(self.unlock, **kwargs)) + def open(self, **kwargs): + """Open the door latch.""" + raise NotImplementedError() + + def async_open(self, **kwargs): + """Open the door latch. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(ft.partial(self.open, **kwargs)) + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/lock/demo.py b/homeassistant/components/lock/demo.py index aca25e7e16d..d561dd333ab 100644 --- a/homeassistant/components/lock/demo.py +++ b/homeassistant/components/lock/demo.py @@ -4,7 +4,7 @@ Demo lock platform that has two fake locks. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockDevice, SUPPORT_OPEN from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) @@ -13,17 +13,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo lock platform.""" add_devices([ DemoLock('Front Door', STATE_LOCKED), - DemoLock('Kitchen Door', STATE_UNLOCKED) + DemoLock('Kitchen Door', STATE_UNLOCKED), + DemoLock('Openable Lock', STATE_LOCKED, True) ]) class DemoLock(LockDevice): """Representation of a Demo lock.""" - def __init__(self, name, state): + def __init__(self, name, state, openable=False): """Initialize the lock.""" self._name = name self._state = state + self._openable = openable @property def should_poll(self): @@ -49,3 +51,14 @@ class DemoLock(LockDevice): """Unlock the device.""" self._state = STATE_UNLOCKED self.schedule_update_ha_state() + + def open(self, **kwargs): + """Open the door latch.""" + self._state = STATE_UNLOCKED + self.schedule_update_ha_state() + + @property + def supported_features(self): + """Flag supported features.""" + if self._openable: + return SUPPORT_OPEN diff --git a/homeassistant/components/lock/homematic.py b/homeassistant/components/lock/homematic.py new file mode 100644 index 00000000000..0d70849e37e --- /dev/null +++ b/homeassistant/components/lock/homematic.py @@ -0,0 +1,58 @@ +""" +Support for Homematic lock. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.homematic/ +""" +import logging +from homeassistant.components.lock import LockDevice, SUPPORT_OPEN +from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES +from homeassistant.const import STATE_UNKNOWN + + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Homematic lock platform.""" + if discovery_info is None: + return + + devices = [] + for conf in discovery_info[ATTR_DISCOVER_DEVICES]: + devices.append(HMLock(conf)) + + add_devices(devices) + + +class HMLock(HMDevice, LockDevice): + """Representation of a Homematic lock aka KeyMatic.""" + + @property + def is_locked(self): + """Return true if the lock is locked.""" + return not bool(self._hm_get_state()) + + def lock(self, **kwargs): + """Lock the lock.""" + self._hmdevice.lock() + + def unlock(self, **kwargs): + """Unlock the lock.""" + self._hmdevice.unlock() + + def open(self, **kwargs): + """Open the door latch.""" + self._hmdevice.open() + + def _init_data_struct(self): + """Generate the data dictionary (self._data) from metadata.""" + self._state = "STATE" + self._data.update({self._state: STATE_UNKNOWN}) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN diff --git a/tests/components/lock/test_demo.py b/tests/components/lock/test_demo.py index 12007d2b8ad..1d774248f35 100644 --- a/tests/components/lock/test_demo.py +++ b/tests/components/lock/test_demo.py @@ -4,11 +4,10 @@ import unittest from homeassistant.setup import setup_component from homeassistant.components import lock -from tests.common import get_test_home_assistant - - +from tests.common import get_test_home_assistant, mock_service FRONT = 'lock.front_door' KITCHEN = 'lock.kitchen_door' +OPENABLE_LOCK = 'lock.openable_lock' class TestLockDemo(unittest.TestCase): @@ -48,3 +47,10 @@ class TestLockDemo(unittest.TestCase): self.hass.block_till_done() self.assertFalse(lock.is_locked(self.hass, FRONT)) + + def test_opening(self): + """Test the opening of a lock.""" + calls = mock_service(self.hass, lock.DOMAIN, lock.SERVICE_OPEN) + lock.open_lock(self.hass, OPENABLE_LOCK) + self.hass.block_till_done() + self.assertEqual(1, len(calls)) From 3e3f710b1269c4045095c899665f6002f3710695 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sun, 25 Mar 2018 23:32:13 +0200 Subject: [PATCH 019/136] Qwikswitch async & updates (#12641) --- homeassistant/components/light/qwikswitch.py | 11 +- homeassistant/components/qwikswitch.py | 150 +++++++++--------- homeassistant/components/switch/qwikswitch.py | 11 +- requirements_all.txt | 2 +- 4 files changed, 87 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/light/qwikswitch.py b/homeassistant/components/light/qwikswitch.py index 63051d2ea8c..c4faf0f9ca0 100644 --- a/homeassistant/components/light/qwikswitch.py +++ b/homeassistant/components/light/qwikswitch.py @@ -6,19 +6,16 @@ https://home-assistant.io/components/light.qwikswitch/ """ import logging -import homeassistant.components.qwikswitch as qwikswitch - -_LOGGER = logging.getLogger(__name__) - DEPENDENCIES = ['qwikswitch'] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the lights from the main Qwikswitch component.""" + """Add lights from the main Qwikswitch component.""" if discovery_info is None: - _LOGGER.error("Configure Qwikswitch component failed") + logging.getLogger(__name__).error( + "Configure Qwikswitch Light component failed") return False - add_devices(qwikswitch.QSUSB['light']) + add_devices(hass.data['qwikswitch']['light']) return True diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 4d5f27082de..c4901805e3e 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -4,18 +4,21 @@ Support for Qwikswitch devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/qwikswitch/ """ +import asyncio import logging import voluptuous as vol from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL) +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import load_platform from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) from homeassistant.components.switch import SwitchDevice -REQUIREMENTS = ['pyqwikswitch==0.4'] +REQUIREMENTS = ['pyqwikswitch==0.5'] _LOGGER = logging.getLogger(__name__) @@ -33,10 +36,6 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_BUTTON_EVENTS): vol.Coerce(str) })}, extra=vol.ALLOW_EXTRA) -QSUSB = {} - -SUPPORT_QWIKSWITCH = SUPPORT_BRIGHTNESS - class QSToggleEntity(object): """Representation of a Qwikswitch Entity. @@ -53,22 +52,15 @@ class QSToggleEntity(object): [3] /components/switch/__init__.py """ - def __init__(self, qsitem, qsusb): + def __init__(self, qsid, qsusb): """Initialize the ToggleEntity.""" - from pyqwikswitch import (QS_ID, QS_NAME, QSType, PQS_VALUE, PQS_TYPE) - self._id = qsitem[QS_ID] - self._name = qsitem[QS_NAME] - self._value = qsitem[PQS_VALUE] - self._qsusb = qsusb - self._dim = qsitem[PQS_TYPE] == QSType.dimmer - QSUSB[self._id] = self + from pyqwikswitch import (QS_NAME, QSDATA, QS_TYPE, QSType) + self._id = qsid + self._qsusb = qsusb.devices + dev = qsusb.devices[qsid] + self._dim = dev[QS_TYPE] == QSType.dimmer + self._name = dev[QSDATA][QS_NAME] - @property - def brightness(self): - """Return the brightness of this light between 0..100.""" - return self._value if self._dim else None - - # pylint: disable=no-self-use @property def should_poll(self): """No polling needed.""" @@ -82,29 +74,27 @@ class QSToggleEntity(object): @property def is_on(self): """Check if device is on (non-zero).""" - return self._value > 0 - - def update_value(self, value): - """Decode the QSUSB value and update the Home assistant state.""" - if value != self._value: - self._value = value - # pylint: disable=no-member - super().schedule_update_ha_state() # Part of Entity/ToggleEntity - return self._value + return self._qsusb[self._id, 1] > 0 def turn_on(self, **kwargs): """Turn the device on.""" - newvalue = 255 - if ATTR_BRIGHTNESS in kwargs: - newvalue = kwargs[ATTR_BRIGHTNESS] - if self._qsusb.set(self._id, round(min(newvalue, 255)/2.55)) >= 0: - self.update_value(newvalue) + new = kwargs.get(ATTR_BRIGHTNESS, 255) + self._qsusb.set_value(self._id, new) - # pylint: disable=unused-argument - def turn_off(self, **kwargs): + @asyncio.coroutine + def async_turn_on(self, **kwargs): + """Turn the device on.""" + new = kwargs.get(ATTR_BRIGHTNESS, 255) + self._qsusb.set_value(self._id, new) + + def turn_off(self, **kwargs): # pylint: disable=unused-argument """Turn the device off.""" - if self._qsusb.set(self._id, 0) >= 0: - self.update_value(0) + self._qsusb.set_value(self._id, 0) + + @asyncio.coroutine + def async_turn_off(self, **kwargs): # pylint: disable=unused-argument + """Turn the device off.""" + self._qsusb.set_value(self._id, 0) class QSSwitch(QSToggleEntity, SwitchDevice): @@ -116,17 +106,25 @@ class QSSwitch(QSToggleEntity, SwitchDevice): class QSLight(QSToggleEntity, Light): """Light based on a Qwikswitch relay/dimmer module.""" + @property + def brightness(self): + """Return the brightness of this light (0-255).""" + return self._qsusb[self._id, 1] if self._dim else None + @property def supported_features(self): """Flag supported features.""" - return SUPPORT_QWIKSWITCH + return SUPPORT_BRIGHTNESS if self._dim else None -def setup(hass, config): - """Set up the QSUSB component.""" +@asyncio.coroutine +def async_setup(hass, config): + """Setup qwiskswitch component.""" + from pyqwikswitch.async import QSUsb from pyqwikswitch import ( - QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD, PQS_VALUE, PQS_TYPE, - QSType) + CMD_BUTTONS, QS_CMD, QSDATA, QS_ID, QS_NAME, QS_TYPE, QSType) + + hass.data[DOMAIN] = {} # Override which cmd's in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] @@ -136,61 +134,69 @@ def setup(hass, config): url = config[DOMAIN][CONF_URL] dimmer_adjust = config[DOMAIN][CONF_DIMMER_ADJUST] - qsusb = QSUsb(url, _LOGGER, dimmer_adjust) + def callback_value_changed(qsdevices, key, new): \ + # pylint: disable=unused-argument + """Update entiry values based on device change.""" + entity = hass.data[DOMAIN].get(key) + if entity is not None: + entity.schedule_update_ha_state() # Part of Entity/ToggleEntity - def _stop(event): + session = async_get_clientsession(hass) + qsusb = QSUsb(url=url, dim_adj=dimmer_adjust, session=session, + callback_value_changed=callback_value_changed) + + @callback + def async_stop(event): # pylint: disable=unused-argument """Stop the listener queue and clean up.""" nonlocal qsusb qsusb.stop() qsusb = None - global QSUSB - QSUSB = {} + hass.data[DOMAIN] = {} _LOGGER.info("Waiting for long poll to QSUSB to time out") - hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _stop) + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop) # Discover all devices in QSUSB - devices = qsusb.devices() - QSUSB['switch'] = [] - QSUSB['light'] = [] - for item in devices: - if item[PQS_TYPE] == QSType.relay and (item[QS_NAME].lower() - .endswith(' switch')): - item[QS_NAME] = item[QS_NAME][:-7] # Remove ' switch' postfix - QSUSB['switch'].append(QSSwitch(item, qsusb)) - elif item[PQS_TYPE] in [QSType.relay, QSType.dimmer]: - QSUSB['light'].append(QSLight(item, qsusb)) + yield from qsusb.update_from_devices() + hass.data[DOMAIN]['switch'] = [] + hass.data[DOMAIN]['light'] = [] + for _id, item in qsusb.devices: + if (item[QS_TYPE] == QSType.relay and + item[QSDATA][QS_NAME].lower().endswith(' switch')): + item[QSDATA][QS_NAME] = item[QSDATA][QS_NAME][:-7] # Remove switch + new_dev = QSSwitch(_id, qsusb) + hass.data[DOMAIN]['switch'].append(new_dev) + elif item[QS_TYPE] in [QSType.relay, QSType.dimmer]: + new_dev = QSLight(_id, qsusb) + hass.data[DOMAIN]['light'].append(new_dev) else: _LOGGER.warning("Ignored unknown QSUSB device: %s", item) + continue + hass.data[DOMAIN][_id] = new_dev # Load platforms for comp_name in ('switch', 'light'): - if QSUSB[comp_name]: + if hass.data[DOMAIN][comp_name]: load_platform(hass, comp_name, 'qwikswitch', {}, config) - def qs_callback(item): + def callback_qs_listen(item): """Typically a button press or update signal.""" if qsusb is None: # Shutting down - _LOGGER.info("Button press or updating signal done") return # If button pressed, fire a hass event - if item.get(QS_CMD, '') in cmd_buttons: - hass.bus.fire('qwikswitch.button.' + item.get(QS_ID, '@no_id')) + if item.get(QS_CMD, '') in cmd_buttons and QS_ID in item: + hass.bus.async_fire('qwikswitch.button.{}'.format(item[QS_ID])) return # Update all ha_objects - qsreply = qsusb.devices() - if qsreply is False: - return - for itm in qsreply: - if itm[QS_ID] in QSUSB: - QSUSB[itm[QS_ID]].update_value( - round(min(itm[PQS_VALUE], 100) * 2.55)) + hass.async_add_job(qsusb.update_from_devices) - def _start(event): + @callback + def async_start(event): # pylint: disable=unused-argument """Start listening.""" - qsusb.listen(callback=qs_callback, timeout=30) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start) + hass.async_add_job(qsusb.listen, callback_qs_listen) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start) return True diff --git a/homeassistant/components/switch/qwikswitch.py b/homeassistant/components/switch/qwikswitch.py index 7aea1dea1e1..258e1141052 100644 --- a/homeassistant/components/switch/qwikswitch.py +++ b/homeassistant/components/switch/qwikswitch.py @@ -6,19 +6,16 @@ https://home-assistant.io/components/switch.qwikswitch/ """ import logging -import homeassistant.components.qwikswitch as qwikswitch - -_LOGGER = logging.getLogger(__name__) - DEPENDENCIES = ['qwikswitch'] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Add switched from the main Qwikswitch component.""" + """Add switches from the main Qwikswitch component.""" if discovery_info is None: - _LOGGER.error("Configure Qwikswitch component") + logging.getLogger(__name__).error( + "Configure Qwikswitch Switch component failed") return False - add_devices(qwikswitch.QSUSB['switch']) + add_devices(hass.data['qwikswitch']['switch']) return True diff --git a/requirements_all.txt b/requirements_all.txt index 79e371e0b48..e51bbc98823 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -860,7 +860,7 @@ pyowm==2.8.0 pypollencom==1.1.1 # homeassistant.components.qwikswitch -pyqwikswitch==0.4 +pyqwikswitch==0.5 # homeassistant.components.rainbird pyrainbird==0.1.3 From 8ab5978db3b6d241546bd6f2d87c85a88ebec48c Mon Sep 17 00:00:00 2001 From: Beat <508289+bdurrer@users.noreply.github.com> Date: Mon, 26 Mar 2018 03:02:21 +0200 Subject: [PATCH 020/136] Fix encoding errors in mikrotik device tracker (#13464) --- homeassistant/components/device_tracker/mikrotik.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py index 1d9161c0d45..154fc3d2a63 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -73,7 +73,8 @@ class MikrotikScanner(DeviceScanner): self.host, self.username, self.password, - port=int(self.port) + port=int(self.port), + encoding='utf-8' ) try: From a5ae77ab93811948c95e579667719971ec47ad3b Mon Sep 17 00:00:00 2001 From: Cedric Van Goethem Date: Mon, 26 Mar 2018 02:03:23 +0100 Subject: [PATCH 021/136] Add extra check for ESSID field in case there's a wired connection (#13459) --- homeassistant/components/device_tracker/unifi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index 8663930c4e6..d8a52aaaeb4 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -98,7 +98,8 @@ class UnifiScanner(DeviceScanner): # Filter clients to provided SSID list if self._ssid_filter: clients = [client for client in clients - if client['essid'] in self._ssid_filter] + if 'essid' in client and + client['essid'] in self._ssid_filter] self._clients = { client['mac']: client From d6af26b589a504317c8b9cfbfc3fac24e8775087 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Mar 2018 18:04:20 -0700 Subject: [PATCH 022/136] Add version bump script (#13447) * Add version bump script * Lint --- script/version_bump.py | 135 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100755 script/version_bump.py diff --git a/script/version_bump.py b/script/version_bump.py new file mode 100755 index 00000000000..0cd02ddbfcb --- /dev/null +++ b/script/version_bump.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Helper script to bump the current version.""" +import argparse +import re + +from homeassistant import const + + +PARSE_PATCH = r'(?P\d+)(\.(?P\D+)(?P\d+))?' + + +def format_patch(patch_parts): + """Format the patch parts back into a patch string.""" + return '{patch}.{prerel}{prerelversion}'.format(**patch_parts) + + +def bump_version(cur_major, cur_minor, cur_patch, bump_type): + """Return a new version given a current version and action.""" + patch_parts = re.match(PARSE_PATCH, cur_patch).groupdict() + patch_parts['patch'] = int(patch_parts['patch']) + if patch_parts['prerelversion'] is not None: + patch_parts['prerelversion'] = int(patch_parts['prerelversion']) + + if bump_type == 'release_patch': + # Convert 0.67.3 to 0.67.4 + # Convert 0.67.3.beta5 to 0.67.3 + # Convert 0.67.3.dev0 to 0.67.3 + new_major = cur_major + new_minor = cur_minor + + if patch_parts['prerel'] is None: + new_patch = str(patch_parts['patch'] + 1) + else: + new_patch = str(patch_parts['patch']) + + elif bump_type == 'dev': + # Convert 0.67.3 to 0.67.4.dev0 + # Convert 0.67.3.beta5 to 0.67.4.dev0 + # Convert 0.67.3.dev0 to 0.67.3.dev1 + new_major = cur_major + + if patch_parts['prerel'] == 'dev': + new_minor = cur_minor + patch_parts['prerelversion'] += 1 + new_patch = format_patch(patch_parts) + else: + new_minor = cur_minor + 1 + new_patch = '0.dev0' + + elif bump_type == 'beta': + # Convert 0.67.5 to 0.67.8.beta0 + # Convert 0.67.0.dev0 to 0.67.0.beta0 + # Convert 0.67.5.beta4 to 0.67.5.beta5 + new_major = cur_major + new_minor = cur_minor + + if patch_parts['prerel'] is None: + patch_parts['patch'] += 1 + patch_parts['prerel'] = 'beta' + patch_parts['prerelversion'] = 0 + + elif patch_parts['prerel'] == 'beta': + patch_parts['prerelversion'] += 1 + + elif patch_parts['prerel'] == 'dev': + patch_parts['prerel'] = 'beta' + patch_parts['prerelversion'] = 0 + + else: + raise Exception('Can only bump from beta or no prerel version') + + new_patch = format_patch(patch_parts) + + return new_major, new_minor, new_patch + + +def write_version(major, minor, patch): + """Update Home Assistant constant file with new version.""" + with open('homeassistant/const.py') as fil: + content = fil.read() + + content = re.sub('MAJOR_VERSION = .*\n', + 'MAJOR_VERSION = {}\n'.format(major), + content) + content = re.sub('MINOR_VERSION = .*\n', + 'MINOR_VERSION = {}\n'.format(minor), + content) + content = re.sub('PATCH_VERSION = .*\n', + "PATCH_VERSION = '{}'\n".format(patch), + content) + + with open('homeassistant/const.py', 'wt') as fil: + content = fil.write(content) + + +def main(): + """Execute script.""" + parser = argparse.ArgumentParser( + description="Bump version of Home Assistant") + parser.add_argument( + 'type', + help="The type of the bump the version to.", + choices=['beta', 'dev', 'release_patch'], + ) + arguments = parser.parse_args() + write_version(*bump_version(const.MAJOR_VERSION, const.MINOR_VERSION, + const.PATCH_VERSION, arguments.type)) + + +def test_bump_version(): + """Make sure it all works.""" + assert bump_version(0, 56, '0', 'beta') == \ + (0, 56, '1.beta0') + assert bump_version(0, 56, '0.beta3', 'beta') == \ + (0, 56, '0.beta4') + assert bump_version(0, 56, '0.dev0', 'beta') == \ + (0, 56, '0.beta0') + + assert bump_version(0, 56, '3', 'dev') == \ + (0, 57, '0.dev0') + assert bump_version(0, 56, '0.beta3', 'dev') == \ + (0, 57, '0.dev0') + assert bump_version(0, 56, '0.dev0', 'dev') == \ + (0, 56, '0.dev1') + + assert bump_version(0, 56, '3', 'release_patch') == \ + (0, 56, '4') + assert bump_version(0, 56, '3.beta3', 'release_patch') == \ + (0, 56, '3') + assert bump_version(0, 56, '0.dev0', 'release_patch') == \ + (0, 56, '0') + + +if __name__ == '__main__': + main() From 1887bac37e6858796c7683ae207457baf49ffeab Mon Sep 17 00:00:00 2001 From: a-andre Date: Mon, 26 Mar 2018 03:07:26 +0200 Subject: [PATCH 023/136] Hyperion: fix typo (#13429) --- homeassistant/components/light/hyperion.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py index e5a4bd18115..8ba2329af7e 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -215,7 +215,7 @@ class Hyperion(Light): pass led_color = response['info']['activeLedColor'] - if not led_color or led_color[0]['RGB value'] == [0, 0, 0]: + if not led_color or led_color[0]['RGB Value'] == [0, 0, 0]: # Get the active effect if response['info'].get('activeEffects'): self._rgb_color = [175, 0, 255] @@ -234,8 +234,7 @@ class Hyperion(Light): self._effect = None else: # Get the RGB color - self._rgb_color =\ - response['info']['activeLedColor'][0]['RGB Value'] + self._rgb_color = led_color[0]['RGB Value'] self._brightness = max(self._rgb_color) self._rgb_mem = [int(round(float(x)*255/self._brightness)) for x in self._rgb_color] From a6e455a07054204b74e224913f95eeac0d5bc5d5 Mon Sep 17 00:00:00 2001 From: Dan Nixon Date: Mon, 26 Mar 2018 17:22:21 +0100 Subject: [PATCH 024/136] Make Telnet Switch value template optional (#13433) When no statis command is defined a value template does nothing so should not have to be provided. --- homeassistant/components/switch/telnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/telnet.py b/homeassistant/components/switch/telnet.py index 7c69b31aa00..c3a608b9692 100644 --- a/homeassistant/components/switch/telnet.py +++ b/homeassistant/components/switch/telnet.py @@ -25,7 +25,7 @@ SWITCH_SCHEMA = vol.Schema({ vol.Required(CONF_COMMAND_OFF): cv.string, vol.Required(CONF_COMMAND_ON): cv.string, vol.Required(CONF_RESOURCE): cv.string, - vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_COMMAND_STATE): cv.string, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, From 2e3ec121d13062061305e8d9106035032f21795e Mon Sep 17 00:00:00 2001 From: Lindsay Ward Date: Tue, 27 Mar 2018 02:27:53 +1000 Subject: [PATCH 025/136] Update yeelightsunflower to 0.0.10 (#13448) * Update yeelightsunflower to 0.0.10 * Update yeelightsunflower platform to 0.0.10 --- homeassistant/components/light/yeelightsunflower.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/yeelightsunflower.py b/homeassistant/components/light/yeelightsunflower.py index 88f86063c13..96cce67b1bb 100644 --- a/homeassistant/components/light/yeelightsunflower.py +++ b/homeassistant/components/light/yeelightsunflower.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( from homeassistant.const import CONF_HOST import homeassistant.util.color as color_util -REQUIREMENTS = ['yeelightsunflower==0.0.8'] +REQUIREMENTS = ['yeelightsunflower==0.0.10'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5b2be9dcbde..bd32ed12d9d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1307,7 +1307,7 @@ yahooweather==0.10 yeelight==0.4.0 # homeassistant.components.light.yeelightsunflower -yeelightsunflower==0.0.8 +yeelightsunflower==0.0.10 # homeassistant.components.media_extractor youtube_dl==2018.03.10 From 181e68b0278a6afd81e1557b7c0b5bbc64749893 Mon Sep 17 00:00:00 2001 From: c727 Date: Mon, 26 Mar 2018 19:22:05 +0200 Subject: [PATCH 026/136] Add more info to issue template (#12955) * Update ISSUE_TEMPLATE.md * Minumum supported version is Python 3.5.3 * typo * Feedback * Feedback * Address comments --- .github/ISSUE_TEMPLATE.md | 57 ++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index c570b548360..e853ce2f1b4 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,23 +1,50 @@ -Make sure you are running the latest version of Home Assistant before reporting an issue. + -You should only file an issue if you found a bug. Feature and enhancement requests should go in [the Feature Requests section](https://community.home-assistant.io/c/feature-requests) of our community forum: +**1. Home Assistant version:** + +``` -**Home Assistant release (`hass --version`):** +``` + +**2.a) I run Hass.io or the Docker image**: + +``` + +``` + +**2.b) ...No, I run an installation with this Python version:** + +``` + +``` + +**3. Component/platform:** + -**Python release (`python3 --version`):** +**4. Description of problem:** -**Component/platform:** +**5. Expected:** -**Description of problem:** - - -**Expected:** - - -**Problem-relevant `configuration.yaml` entries and steps to reproduce:** +**6. Problem-relevant `configuration.yaml` entries and steps to reproduce:** ```yaml ``` @@ -26,10 +53,10 @@ You should only file an issue if you found a bug. Feature and enhancement reques 2. 3. -**Traceback (if applicable):** -```bash +**7. Traceback (if applicable):** +``` ``` -**Additional info:** +**8. Additional info:** From 3e6f4d0e5acb3ea70e439501541c1f15d98b115e Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 26 Mar 2018 21:21:18 +0200 Subject: [PATCH 027/136] [RFC] Update issue template (#12989) * Update issue template * Any release --- .github/ISSUE_TEMPLATE.md | 47 +++++++++++++-------------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index e853ce2f1b4..84464220749 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,62 +1,45 @@ -**1. Home Assistant version:** +**Home Assistant release with the issue:** -``` -``` -**2.a) I run Hass.io or the Docker image**: +**Last working Home Assistant release (if known):** + + +**Operating environment (Hass.io/Docker/Windows/etc.):** -``` -``` - -**2.b) ...No, I run an installation with this Python version:** +**Component/platform:** -``` - -``` - -**3. Component/platform:** - -**4. Description of problem:** +**Description of problem:** -**5. Expected:** - -**6. Problem-relevant `configuration.yaml` entries and steps to reproduce:** +**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):** ```yaml ``` -1. -2. -3. - -**7. Traceback (if applicable):** +**Traceback (if applicable):** ``` ``` -**8. Additional info:** +**Additional information:** From 263dbe5d81af3667227bee8183e9bfd679ab25ca Mon Sep 17 00:00:00 2001 From: phileaton Date: Mon, 26 Mar 2018 12:32:38 -0700 Subject: [PATCH 028/136] Update total_connect_client to 0.17 for Honeywell L5100-WiFi Support (#13473) * Update total_connect_client to 0.17 * Delete tqdm.1 --- homeassistant/components/alarm_control_panel/totalconnect.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py index 5c1323989d4..1f383e32f92 100644 --- a/homeassistant/components/alarm_control_panel/totalconnect.py +++ b/homeassistant/components/alarm_control_panel/totalconnect.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_CUSTOM_BYPASS) -REQUIREMENTS = ['total_connect_client==0.16'] +REQUIREMENTS = ['total_connect_client==0.17'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index bd32ed12d9d..2dfd359c1f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1218,7 +1218,7 @@ todoist-python==7.0.17 toonlib==1.0.2 # homeassistant.components.alarm_control_panel.totalconnect -total_connect_client==0.16 +total_connect_client==0.17 # homeassistant.components.sensor.transmission # homeassistant.components.switch.transmission From 24e0bb198a8f809afce156708fc9345b60a3a56a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 14:00:56 -0700 Subject: [PATCH 029/136] Hue: Convert XY to HS color if HS not present (#13465) * Hue: Convert XY to HS color if HS not present * Revert change to test * Address comments * Lint --- homeassistant/components/light/hue.py | 30 ++++++++------ tests/components/light/test_hue.py | 57 +++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 71e3d4fa30b..4a54f0a337d 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -18,6 +18,7 @@ from homeassistant.components.light import ( FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, SUPPORT_TRANSITION, Light) +from homeassistant.util import color DEPENDENCIES = ['hue'] SCAN_INTERVAL = timedelta(seconds=5) @@ -235,19 +236,26 @@ class HueLight(Light): @property def hs_color(self): """Return the hs color value.""" - # Don't return hue/sat if in color temperature mode - if self._color_mode == "ct": + # pylint: disable=redefined-outer-name + mode = self._color_mode + + if mode not in ('hs', 'xy'): + return + + source = self.light.action if self.is_group else self.light.state + + hue = source.get('hue') + sat = source.get('sat') + + # Sometimes the state will not include valid hue/sat values. + # Reported as issue 13434 + if hue is not None and sat is not None: + return hue / 65535 * 360, sat / 255 * 100 + + if 'xy' not in source: return None - if self.is_group: - return ( - self.light.action.get('hue') / 65535 * 360, - self.light.action.get('sat') / 255 * 100, - ) - return ( - self.light.state.get('hue') / 65535 * 360, - self.light.state.get('sat') / 255 * 100, - ) + return color.color_xy_to_hs(*source['xy']) @property def color_temp(self): diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 54bb2184a64..d73531b1b9a 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -11,6 +11,7 @@ import pytest from homeassistant.components import hue import homeassistant.components.light.hue as hue_light +from homeassistant.util import color _LOGGER = logging.getLogger(__name__) @@ -623,3 +624,59 @@ def test_available(): ) assert light.available is True + + +def test_hs_color(): + """Test hs_color property.""" + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'ct', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color is None + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'hs', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'xy', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'xy', + 'hue': None, + 'sat': 123, + 'xy': [0.4, 0.5] + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == color.color_xy_to_hs(0.4, 0.5) From 08bcf841709e1c2fbc632d99bc970337b22ec8fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 14:55:09 -0700 Subject: [PATCH 030/136] version should contain just 'b' not 'beta' (#13476) --- script/version_bump.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/script/version_bump.py b/script/version_bump.py index 0cd02ddbfcb..0500fc45957 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -23,7 +23,7 @@ def bump_version(cur_major, cur_minor, cur_patch, bump_type): if bump_type == 'release_patch': # Convert 0.67.3 to 0.67.4 - # Convert 0.67.3.beta5 to 0.67.3 + # Convert 0.67.3.b5 to 0.67.3 # Convert 0.67.3.dev0 to 0.67.3 new_major = cur_major new_minor = cur_minor @@ -35,7 +35,7 @@ def bump_version(cur_major, cur_minor, cur_patch, bump_type): elif bump_type == 'dev': # Convert 0.67.3 to 0.67.4.dev0 - # Convert 0.67.3.beta5 to 0.67.4.dev0 + # Convert 0.67.3.b5 to 0.67.4.dev0 # Convert 0.67.3.dev0 to 0.67.3.dev1 new_major = cur_major @@ -48,22 +48,22 @@ def bump_version(cur_major, cur_minor, cur_patch, bump_type): new_patch = '0.dev0' elif bump_type == 'beta': - # Convert 0.67.5 to 0.67.8.beta0 - # Convert 0.67.0.dev0 to 0.67.0.beta0 - # Convert 0.67.5.beta4 to 0.67.5.beta5 + # Convert 0.67.5 to 0.67.8.b0 + # Convert 0.67.0.dev0 to 0.67.0.b0 + # Convert 0.67.5.b4 to 0.67.5.b5 new_major = cur_major new_minor = cur_minor if patch_parts['prerel'] is None: patch_parts['patch'] += 1 - patch_parts['prerel'] = 'beta' + patch_parts['prerel'] = 'b' patch_parts['prerelversion'] = 0 - elif patch_parts['prerel'] == 'beta': + elif patch_parts['prerel'] == 'b': patch_parts['prerelversion'] += 1 elif patch_parts['prerel'] == 'dev': - patch_parts['prerel'] = 'beta' + patch_parts['prerel'] = 'b' patch_parts['prerelversion'] = 0 else: @@ -110,22 +110,22 @@ def main(): def test_bump_version(): """Make sure it all works.""" assert bump_version(0, 56, '0', 'beta') == \ - (0, 56, '1.beta0') - assert bump_version(0, 56, '0.beta3', 'beta') == \ - (0, 56, '0.beta4') + (0, 56, '1.b0') + assert bump_version(0, 56, '0.b3', 'beta') == \ + (0, 56, '0.b4') assert bump_version(0, 56, '0.dev0', 'beta') == \ - (0, 56, '0.beta0') + (0, 56, '0.b0') assert bump_version(0, 56, '3', 'dev') == \ (0, 57, '0.dev0') - assert bump_version(0, 56, '0.beta3', 'dev') == \ + assert bump_version(0, 56, '0.b3', 'dev') == \ (0, 57, '0.dev0') assert bump_version(0, 56, '0.dev0', 'dev') == \ (0, 56, '0.dev1') assert bump_version(0, 56, '3', 'release_patch') == \ (0, 56, '4') - assert bump_version(0, 56, '3.beta3', 'release_patch') == \ + assert bump_version(0, 56, '3.b3', 'release_patch') == \ (0, 56, '3') assert bump_version(0, 56, '0.dev0', 'release_patch') == \ (0, 56, '0') From f1d37fc8494225515e4729affc1d58a5a9915f2d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 16:07:22 -0700 Subject: [PATCH 031/136] Upgrade aiohue and fix race condition (#13475) * Bump aiohue to 1.3 * Store bridge in hass.data before setting up platform * Fix tests --- homeassistant/components/hue/__init__.py | 5 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/test_bridge.py | 1 + tests/components/hue/test_setup.py | 6 +----- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 2fb55f8f6e0..b70021e0304 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers import discovery, aiohttp_client from homeassistant import config_entries from homeassistant.util.json import save_json -REQUIREMENTS = ['aiohue==1.2.0'] +REQUIREMENTS = ['aiohue==1.3.0'] _LOGGER = logging.getLogger(__name__) @@ -145,7 +145,6 @@ async def async_setup_bridge( bridge = HueBridge(host, hass, filename, username, allow_unreachable, allow_hue_groups) await bridge.async_setup() - hass.data[DOMAIN][host] = bridge def _find_username_from_config(hass, filename): @@ -209,6 +208,8 @@ class HueBridge(object): self.host) return + self.hass.data[DOMAIN][self.host] = self + # If we came here and configuring this host, mark as done if self.config_request_id: request_id = self.config_request_id diff --git a/requirements_all.txt b/requirements_all.txt index 2dfd359c1f1..e8baacf7d2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -74,7 +74,7 @@ aiodns==1.1.1 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.2.0 +aiohue==1.3.0 # homeassistant.components.sensor.imap aioimaplib==0.7.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7610eb02d2..185f1fff81b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -35,7 +35,7 @@ aioautomatic==0.6.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.2.0 +aiohue==1.3.0 # homeassistant.components.notify.apns apns2==0.3.0 diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 88a7223d91e..39351699df5 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -66,6 +66,7 @@ async def test_only_create_no_username(hass): async def test_configurator_callback(hass, mock_request): """.""" + hass.data[hue.DOMAIN] = {} with patch('aiohue.Bridge.create_user', side_effect=aiohue.LinkButtonNotPressed): await MockBridge(hass).async_setup() diff --git a/tests/components/hue/test_setup.py b/tests/components/hue/test_setup.py index 690419fcb7a..f90f58a50c3 100644 --- a/tests/components/hue/test_setup.py +++ b/tests/components/hue/test_setup.py @@ -18,7 +18,6 @@ async def test_setup_with_multiple_hosts(hass, mock_bridge): assert len(mock_bridge.mock_calls) == 2 hosts = sorted(mock_call[1][0] for mock_call in mock_bridge.mock_calls) assert hosts == ['127.0.0.1', '192.168.1.10'] - assert len(hass.data[hue.DOMAIN]) == 2 async def test_bridge_discovered(hass, mock_bridge): @@ -33,7 +32,6 @@ async def test_bridge_discovered(hass, mock_bridge): assert len(mock_bridge.mock_calls) == 1 assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' - assert len(hass.data[hue.DOMAIN]) == 1 async def test_bridge_configure_and_discovered(hass, mock_bridge): @@ -48,7 +46,7 @@ async def test_bridge_configure_and_discovered(hass, mock_bridge): assert len(mock_bridge.mock_calls) == 1 assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' - assert len(hass.data[hue.DOMAIN]) == 1 + hass.data[hue.DOMAIN] = {'192.168.1.10': {}} mock_bridge.reset_mock() @@ -59,7 +57,6 @@ async def test_bridge_configure_and_discovered(hass, mock_bridge): await hass.async_block_till_done() assert len(mock_bridge.mock_calls) == 0 - assert len(hass.data[hue.DOMAIN]) == 1 async def test_setup_no_host(hass, aioclient_mock): @@ -71,4 +68,3 @@ async def test_setup_no_host(hass, aioclient_mock): assert result assert len(aioclient_mock.mock_calls) == 1 - assert len(hass.data[hue.DOMAIN]) == 0 From 254256c08ff83586bc6dbf2d439192a5667cdce3 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 27 Mar 2018 01:08:44 +0200 Subject: [PATCH 032/136] Fix ID (fixes #13444) (#13471) --- homeassistant/components/media_player/mpchc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py index cc195db2590..a375a585ad4 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -155,8 +155,8 @@ class MpcHcDevice(MediaPlayerDevice): def media_next_track(self): """Send next track command.""" - self._send_command(921) + self._send_command(920) def media_previous_track(self): """Send previous track command.""" - self._send_command(920) + self._send_command(919) From 81cf0dacfe27d0cb7105f539ff3ae01fd02fdcd3 Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Mon, 26 Mar 2018 19:10:22 -0600 Subject: [PATCH 033/136] Fix Google Calendar caching when offline (#13375) * Fix Google Calendar caching when offline Events from Google Calendar were not firing under the following circumstances: 1. Start ha as normal with Google Calendar configured as per instructions. 2. ha loses network connectivity to Google 3. ha attempts update of Google Calendar 4. calendar/google component throws uncaught Exception causing update method to not return 5. (cached) Google Calendar event does not fire, remains "Off" Catching the Exception and returning False from the update() method causes the correct behavior (i.e., the calendar component firing the event as scheduled using cached data). * Add requirements * Revert code cleanup * Remove explicit return value from update() * Revert "Remove explicit return value from update()" This reverts commit 7cd77708af658ccea855de47a32ce4ac5262ac30. * Use MockDependency decorator No need to whitelist google-python-api-client for a single unit test at this point. --- homeassistant/components/calendar/google.py | 9 ++++++++- tests/components/calendar/test_google.py | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index 098c7c70834..a8763e8ca9e 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -62,7 +62,14 @@ class GoogleCalendarData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" - service = self.calendar_service.get() + from httplib2 import ServerNotFoundError + + try: + service = self.calendar_service.get() + except ServerNotFoundError: + _LOGGER.warning("Unable to connect to Google, using cached data") + return False + params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) params['timeMin'] = dt.now().isoformat('T') params['calendarId'] = self.calendar_id diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py index 62c8ea8854f..9f94ea9f44c 100644 --- a/tests/components/calendar/test_google.py +++ b/tests/components/calendar/test_google.py @@ -2,7 +2,7 @@ # pylint: disable=protected-access import logging import unittest -from unittest.mock import patch +from unittest.mock import patch, Mock import pytest @@ -11,7 +11,7 @@ import homeassistant.components.calendar.google as calendar import homeassistant.util.dt as dt_util from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers.template import DATE_STR_FORMAT -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, MockDependency TEST_PLATFORM = {calendar_base.DOMAIN: {CONF_PLATFORM: 'test'}} @@ -421,3 +421,16 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'location': event['location'], 'description': event['description'] }) + + @MockDependency("httplib2") + def test_update_false(self, mock_httplib2): + """Test that the update returns False upon Error.""" + mock_service = Mock() + mock_service.get = Mock( + side_effect=mock_httplib2.ServerNotFoundError("unit test")) + + cal = calendar.GoogleCalendarEventDevice(self.hass, mock_service, None, + {'name': "test"}) + result = cal.data.update() + + self.assertFalse(result) From 8a0facb747e59867a135b6fcc429b1c1d55feebf Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Mar 2018 04:50:29 +0200 Subject: [PATCH 034/136] Validate basic customize entries (#13478) * Added schema to validate customize dictionary * Added test --- homeassistant/config.py | 13 ++++++++++--- tests/test_config.py | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 58cfe845e8f..53e611ac725 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -13,6 +13,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, CONF_UNIT_SYSTEM, CONF_TIME_ZONE, CONF_ELEVATION, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, @@ -129,13 +130,19 @@ PACKAGES_CONFIG_SCHEMA = vol.Schema({ {cv.slug: vol.Any(dict, list, None)}) # Only slugs for component names }) +CUSTOMIZE_DICT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_FRIENDLY_NAME): cv.string, + vol.Optional(ATTR_HIDDEN): cv.boolean, + vol.Optional(ATTR_ASSUMED_STATE): cv.boolean, +}, extra=vol.ALLOW_EXTRA) + CUSTOMIZE_CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_CUSTOMIZE, default={}): - vol.Schema({cv.entity_id: dict}), + vol.Schema({cv.entity_id: CUSTOMIZE_DICT_SCHEMA}), vol.Optional(CONF_CUSTOMIZE_DOMAIN, default={}): - vol.Schema({cv.string: dict}), + vol.Schema({cv.string: CUSTOMIZE_DICT_SCHEMA}), vol.Optional(CONF_CUSTOMIZE_GLOB, default={}): - vol.Schema({cv.string: OrderedDict}), + vol.Schema({cv.string: CUSTOMIZE_DICT_SCHEMA}), }) CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend({ diff --git a/tests/test_config.py b/tests/test_config.py index aaa793f91a9..22fcebc6ea4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -12,6 +12,7 @@ from voluptuous import MultipleInvalid from homeassistant.core import DOMAIN, HomeAssistantError, Config import homeassistant.config as config_util from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIT_SYSTEM, CONF_NAME, CONF_TIME_ZONE, CONF_ELEVATION, CONF_CUSTOMIZE, __version__, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT) @@ -235,6 +236,29 @@ class TestConfig(unittest.TestCase): }, }) + def test_customize_dict_schema(self): + """Test basic customize config validation.""" + values = ( + {ATTR_FRIENDLY_NAME: None}, + {ATTR_HIDDEN: '2'}, + {ATTR_ASSUMED_STATE: '2'}, + ) + + for val in values: + print(val) + with pytest.raises(MultipleInvalid): + config_util.CUSTOMIZE_DICT_SCHEMA(val) + + assert config_util.CUSTOMIZE_DICT_SCHEMA({ + ATTR_FRIENDLY_NAME: 2, + ATTR_HIDDEN: '1', + ATTR_ASSUMED_STATE: '0', + }) == { + ATTR_FRIENDLY_NAME: '2', + ATTR_HIDDEN: True, + ATTR_ASSUMED_STATE: False + } + def test_customize_glob_is_ordered(self): """Test that customize_glob preserves order.""" conf = config_util.CORE_CONFIG_SCHEMA( From 9eda04b787a55416e5ea8a183d38e8e39698f6aa Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Mar 2018 11:31:18 +0200 Subject: [PATCH 035/136] Homekit: Bugfix Thermostat Fahrenheit support (#13477) * Bugfix thermostat temperature conversion * util -> temperature_to_homekit * util -> temperature_to_states * util -> convert_to_float * Added tests, deleted log msg --- homeassistant/components/homekit/__init__.py | 3 -- .../components/homekit/type_sensors.py | 35 ++++------------ .../components/homekit/type_thermostats.py | 33 ++++++++++----- homeassistant/components/homekit/util.py | 21 +++++++++- .../homekit/test_get_accessories.py | 14 +++++++ tests/components/homekit/test_type_sensors.py | 21 +--------- .../homekit/test_type_thermostats.py | 41 ++++++++++++++++++- tests/components/homekit/test_util.py | 23 ++++++++++- 8 files changed, 126 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 4854a828e41..8ef8445aa70 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -186,9 +186,6 @@ class HomeKit(): for state in self._hass.states.all(): self.add_bridge_accessory(state) - for entity_id in self._config: - _LOGGER.warning('The entity "%s" was not setup when HomeKit ' - 'was started', entity_id) self.bridge.set_broker(self.driver) if not self.bridge.paired: diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 7575acb5c35..e980ce4a316 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -2,8 +2,7 @@ import logging from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS) -from homeassistant.util.temperature import fahrenheit_to_celsius + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from . import TYPES from .accessories import ( @@ -11,33 +10,12 @@ from .accessories import ( from .const import ( CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) +from .util import convert_to_float, temperature_to_homekit _LOGGER = logging.getLogger(__name__) -def calc_temperature(state, unit=TEMP_CELSIUS): - """Calculate temperature from state and unit. - - Always return temperature as Celsius value. - Conversion is handled on the device. - """ - try: - value = float(state) - except ValueError: - return None - - return fahrenheit_to_celsius(value) if unit == TEMP_FAHRENHEIT else value - - -def calc_humidity(state): - """Calculate humidity from state.""" - try: - return float(state) - except ValueError: - return None - - @TYPES.register('TemperatureSensor') class TemperatureSensor(HomeAccessory): """Generate a TemperatureSensor accessory for a temperature sensor. @@ -63,9 +41,10 @@ class TemperatureSensor(HomeAccessory): if new_state is None: return - unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT] - temperature = calc_temperature(new_state.state, unit) + unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + temperature = convert_to_float(new_state.state) if temperature: + temperature = temperature_to_homekit(temperature, unit) self.char_temp.set_value(temperature, should_callback=False) _LOGGER.debug('%s: Current temperature set to %d°C', self._entity_id, temperature) @@ -92,8 +71,8 @@ class HumiditySensor(HomeAccessory): if new_state is None: return - humidity = calc_humidity(new_state.state) + humidity = convert_to_float(new_state.state) if humidity: self.char_humidity.set_value(humidity, should_callback=False) - _LOGGER.debug('%s: Current humidity set to %d%%', + _LOGGER.debug('%s: Percent set to %d%%', self._entity_id, humidity) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 3f545e90eb3..d49c1ca626b 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -16,6 +16,7 @@ from .const import ( CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) +from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -40,6 +41,7 @@ class Thermostat(HomeAccessory): self._hass = hass self._entity_id = entity_id self._call_timer = None + self._unit = TEMP_CELSIUS self.heat_cool_flag_target_state = False self.temperature_flag_target_state = False @@ -107,33 +109,38 @@ class Thermostat(HomeAccessory): def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set cooling threshold temperature to %.2f', + _LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C', self._entity_id, value) self.coolingthresh_flag_target_state = True self.char_cooling_thresh_temp.set_value(value, should_callback=False) low = self.char_heating_thresh_temp.value + low = temperature_to_states(low, self._unit) + value = temperature_to_states(value, self._unit) self._hass.components.climate.set_temperature( entity_id=self._entity_id, target_temp_high=value, target_temp_low=low) def set_heating_threshold(self, value): """Set heating threshold temp to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set heating threshold temperature to %.2f', + _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', self._entity_id, value) self.heatingthresh_flag_target_state = True self.char_heating_thresh_temp.set_value(value, should_callback=False) # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value + high = temperature_to_states(high, self._unit) + value = temperature_to_states(value, self._unit) self._hass.components.climate.set_temperature( entity_id=self._entity_id, target_temp_high=high, target_temp_low=value) def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set target temperature to %.2f', + _LOGGER.debug('%s: Set target temperature to %.2f°C', self._entity_id, value) self.temperature_flag_target_state = True self.char_target_temp.set_value(value, should_callback=False) + value = temperature_to_states(value, self._unit) self._hass.components.climate.set_temperature( temperature=value, entity_id=self._entity_id) @@ -142,14 +149,19 @@ class Thermostat(HomeAccessory): if new_state is None: return + self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, + TEMP_CELSIUS) + # Update current temperature current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) if isinstance(current_temp, (int, float)): + current_temp = temperature_to_homekit(current_temp, self._unit) self.char_current_temp.set_value(current_temp) # Update target temperature target_temp = new_state.attributes.get(ATTR_TEMPERATURE) if isinstance(target_temp, (int, float)): + target_temp = temperature_to_homekit(target_temp, self._unit) if not self.temperature_flag_target_state: self.char_target_temp.set_value(target_temp, should_callback=False) @@ -158,7 +170,9 @@ class Thermostat(HomeAccessory): # Update cooling threshold temperature if characteristic exists if self.char_cooling_thresh_temp: cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) - if cooling_thresh: + if isinstance(cooling_thresh, (int, float)): + cooling_thresh = temperature_to_homekit(cooling_thresh, + self._unit) if not self.coolingthresh_flag_target_state: self.char_cooling_thresh_temp.set_value( cooling_thresh, should_callback=False) @@ -167,18 +181,17 @@ class Thermostat(HomeAccessory): # Update heating threshold temperature if characteristic exists if self.char_heating_thresh_temp: heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) - if heating_thresh: + if isinstance(heating_thresh, (int, float)): + heating_thresh = temperature_to_homekit(heating_thresh, + self._unit) if not self.heatingthresh_flag_target_state: self.char_heating_thresh_temp.set_value( heating_thresh, should_callback=False) self.heatingthresh_flag_target_state = False # Update display units - display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if display_units \ - and display_units in UNIT_HASS_TO_HOMEKIT: - self.char_display_units.set_value( - UNIT_HASS_TO_HOMEKIT[display_units]) + if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: + self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit]) # Update target operation mode operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index f18eb2273db..2fa2ebd396a 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -5,8 +5,9 @@ import voluptuous as vol from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE) + ATTR_CODE, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv +import homeassistant.util.temperature as temp_util from .const import HOMEKIT_NOTIFY_ID _LOGGER = logging.getLogger(__name__) @@ -44,3 +45,21 @@ def show_setup_message(bridge, hass): def dismiss_setup_message(hass): """Dismiss persistent notification and remove QR code.""" hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID) + + +def convert_to_float(state): + """Return float of state, catch errors.""" + try: + return float(state) + except (ValueError, TypeError): + return None + + +def temperature_to_homekit(temperature, unit): + """Convert temperature to Celsius for HomeKit.""" + return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1) + + +def temperature_to_states(temperature, unit): + """Convert temperature back from Celsius to Home Assistant unit.""" + return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index ee7baae2755..e29ed85b5fc 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -16,6 +16,20 @@ _LOGGER = logging.getLogger(__name__) CONFIG = {} +def test_get_accessory_invalid_aid(caplog): + """Test with unsupported component.""" + assert get_accessory(None, State('light.demo', 'on'), + aid=None, config=None) is None + assert caplog.records[0].levelname == 'WARNING' + assert 'invalid aid' in caplog.records[0].msg + + +def test_not_supported(): + """Test if none is returned if entity isn't supported.""" + assert get_accessory(None, State('demo.demo', 'on'), aid=2, config=None) \ + is None + + class TestGetAccessories(unittest.TestCase): """Methods to test the get_accessory method.""" diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 551dfc6780d..c04c250613d 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -3,32 +3,13 @@ import unittest from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, HumiditySensor, calc_temperature, calc_humidity) + TemperatureSensor, HumiditySensor) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant -def test_calc_temperature(): - """Test if temperature in Celsius is calculated correctly.""" - assert calc_temperature(STATE_UNKNOWN) is None - assert calc_temperature('test') is None - - assert calc_temperature('20') == 20 - assert calc_temperature('20.12', TEMP_CELSIUS) == 20.12 - assert calc_temperature('75.2', TEMP_FAHRENHEIT) == 24 - - -def test_calc_humidity(): - """Test if humidity is a integer.""" - assert calc_humidity(STATE_UNKNOWN) is None - assert calc_humidity('test') is None - - assert calc_humidity('20') == 20 - assert calc_humidity('75.2') == 75.2 - - class TestHomekitSensors(unittest.TestCase): """Test class for all accessory types regarding sensors.""" diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 6505bf72efb..011fe73377d 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -10,7 +10,7 @@ from homeassistant.components.homekit.type_thermostats import ( Thermostat, STATE_OFF) from homeassistant.const import ( ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA, - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant @@ -238,3 +238,42 @@ class TestHomekitThermostats(unittest.TestCase): self.events[1].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_HIGH], 25.0) self.assertEqual(acc.char_cooling_thresh_temp.value, 25.0) + + def test_thermostat_fahrenheit(self): + """Test if accessory and HA are updated accordingly.""" + climate = 'climate.test' + + acc = Thermostat(self.hass, climate, 'Climate', True) + acc.run() + + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 75.2, + ATTR_TARGET_TEMP_LOW: 68, + ATTR_TEMPERATURE: 71.6, + ATTR_CURRENT_TEMPERATURE: 73.4, + ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + self.hass.block_till_done() + self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) + self.assertEqual(acc.char_cooling_thresh_temp.value, 24.0) + self.assertEqual(acc.char_current_temp.value, 23.0) + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_display_units.value, 1) + + # Set from HomeKit + acc.char_cooling_thresh_temp.set_value(23) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) + self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 68) + + acc.char_heating_thresh_temp.set_value(22) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) + self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 71.6) + + acc.char_target_temp.set_value(24.0) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TEMPERATURE], 75.2) diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index f95db9a4a13..d6ef5856f85 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -7,13 +7,15 @@ from homeassistant.core import callback from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( - show_setup_message, dismiss_setup_message, ATTR_CODE) + show_setup_message, dismiss_setup_message, convert_to_float, + temperature_to_homekit, temperature_to_states, ATTR_CODE) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( SERVICE_CREATE, SERVICE_DISMISS, ATTR_NOTIFICATION_ID) from homeassistant.const import ( - EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA) + EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, + TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) from tests.common import get_test_home_assistant @@ -81,3 +83,20 @@ class TestUtil(unittest.TestCase): self.assertEqual( data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), HOMEKIT_NOTIFY_ID) + + def test_convert_to_float(self): + """Test convert_to_float method.""" + self.assertEqual(convert_to_float(12), 12) + self.assertEqual(convert_to_float(12.4), 12.4) + self.assertIsNone(convert_to_float(STATE_UNKNOWN)) + self.assertIsNone(convert_to_float(None)) + + def test_temperature_to_homekit(self): + """Test temperature conversion from HA to HomeKit.""" + self.assertEqual(temperature_to_homekit(20.46, TEMP_CELSIUS), 20.5) + self.assertEqual(temperature_to_homekit(92.1, TEMP_FAHRENHEIT), 33.4) + + def test_temperature_to_states(self): + """Test temperature conversion from HomeKit to HA.""" + self.assertEqual(temperature_to_states(20, TEMP_CELSIUS), 20.0) + self.assertEqual(temperature_to_states(20.2, TEMP_FAHRENHEIT), 68.4) From 06aded1a4db86043d4f49b30dab87879bd816fba Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 27 Mar 2018 13:09:01 +0200 Subject: [PATCH 036/136] Upgrade python-mystrom to 0.4.2 (#13485) --- homeassistant/components/light/mystrom.py | 4 ++-- homeassistant/components/switch/mystrom.py | 6 +++--- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/light/mystrom.py b/homeassistant/components/light/mystrom.py index d9312e6aadc..8d7fb807c6d 100644 --- a/homeassistant/components/light/mystrom.py +++ b/homeassistant/components/light/mystrom.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN -REQUIREMENTS = ['python-mystrom==0.3.8'] +REQUIREMENTS = ['python-mystrom==0.4.2'] _LOGGER = logging.getLogger(__name__) @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the myStrom Light platform.""" - from pymystrom import MyStromBulb + from pymystrom.bulb import MyStromBulb from pymystrom.exceptions import MyStromConnectionError host = config.get(CONF_HOST) diff --git a/homeassistant/components/switch/mystrom.py b/homeassistant/components/switch/mystrom.py index e813da43dfa..0a87d41d2fe 100644 --- a/homeassistant/components/switch/mystrom.py +++ b/homeassistant/components/switch/mystrom.py @@ -12,7 +12,7 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_NAME, CONF_HOST) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-mystrom==0.3.8'] +REQUIREMENTS = ['python-mystrom==0.4.2'] DEFAULT_NAME = 'myStrom Switch' @@ -26,7 +26,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return myStrom switch.""" - from pymystrom import MyStromPlug, exceptions + from pymystrom.switch import MyStromPlug, exceptions name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -45,7 +45,7 @@ class MyStromSwitch(SwitchDevice): def __init__(self, name, resource): """Initialize the myStrom switch.""" - from pymystrom import MyStromPlug + from pymystrom.switch import MyStromPlug self._name = name self._resource = resource diff --git a/requirements_all.txt b/requirements_all.txt index e8baacf7d2e..026892099f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -955,7 +955,7 @@ python-mpd2==0.5.5 # homeassistant.components.light.mystrom # homeassistant.components.switch.mystrom -python-mystrom==0.3.8 +python-mystrom==0.4.2 # homeassistant.components.nest python-nest==3.7.0 From 264be677872ae04b96b9d42f48126258641008ca Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 27 Mar 2018 20:29:18 +0200 Subject: [PATCH 037/136] New service added to control the power mode of the yeelight (#13267) * New service added to control the power mode of the yeelight * Debug output removed. * Strict validation of the available power modes * Service description added * Service parameter name fixed --- homeassistant/components/light/services.yaml | 10 ++++ homeassistant/components/light/yeelight.py | 60 ++++++++++++++++++-- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 44e887e62c4..9645e50d06e 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -179,3 +179,13 @@ xiaomi_miio_set_delayed_turn_off: time_period: description: Time period for the delayed turn off. example: "5, '0:05', {'minutes': 5}" + +yeelight_set_mode: + description: Set a operation mode. + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + mode: + description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'. + example: 'moonlight' diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index 585db950efc..7061c24aac6 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP, ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH, - SUPPORT_EFFECT, Light, PLATFORM_SCHEMA) + SUPPORT_EFFECT, Light, PLATFORM_SCHEMA, ATTR_ENTITY_ID, DOMAIN) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -30,7 +30,7 @@ DEFAULT_TRANSITION = 350 CONF_SAVE_ON_CHANGE = 'save_on_change' CONF_MODE_MUSIC = 'use_music_mode' -DOMAIN = 'yeelight' +DATA_KEY = 'light.yeelight' DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, @@ -90,6 +90,13 @@ YEELIGHT_EFFECT_LIST = [ EFFECT_TWITTER, EFFECT_STOP] +SERVICE_SET_MODE = 'yeelight_set_mode' +ATTR_MODE = 'mode' + +YEELIGHT_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + def _cmd(func): """Define a wrapper to catch exceptions from the bulb.""" @@ -106,6 +113,11 @@ def _cmd(func): def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Yeelight bulbs.""" + from yeelight.enums import PowerMode + + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} + lights = [] if discovery_info is not None: _LOGGER.debug("Adding autodetected %s", discovery_info['hostname']) @@ -115,16 +127,44 @@ def setup_platform(hass, config, add_devices, discovery_info=None): discovery_info['properties']['mac']) device = {'name': name, 'ipaddr': discovery_info['host']} - lights.append(YeelightLight(device, DEVICE_SCHEMA({}))) + light = YeelightLight(device, DEVICE_SCHEMA({})) + lights.append(light) + hass.data[DATA_KEY][name] = light else: for ipaddr, device_config in config[CONF_DEVICES].items(): - _LOGGER.debug("Adding configured %s", device_config[CONF_NAME]) + name = device_config[CONF_NAME] + _LOGGER.debug("Adding configured %s", name) - device = {'name': device_config[CONF_NAME], 'ipaddr': ipaddr} - lights.append(YeelightLight(device, device_config)) + device = {'name': name, 'ipaddr': ipaddr} + light = YeelightLight(device, device_config) + lights.append(light) + hass.data[DATA_KEY][name] = light add_devices(lights, True) + def service_handler(service): + """Dispatch service calls to target entities.""" + params = {key: value for key, value in service.data.items() + if key != ATTR_ENTITY_ID} + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + target_devices = [dev for dev in hass.data[DATA_KEY].values() + if dev.entity_id in entity_ids] + else: + target_devices = hass.data[DATA_KEY].values() + + for target_device in target_devices: + if service.service == SERVICE_SET_MODE: + target_device.set_mode(**params) + + service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_MODE): + vol.In([mode.name.lower() for mode in PowerMode]) + }) + hass.services.register( + DOMAIN, SERVICE_SET_MODE, service_handler, + schema=service_schema_set_mode) + class YeelightLight(Light): """Representation of a Yeelight light.""" @@ -444,3 +484,11 @@ class YeelightLight(Light): self._bulb.turn_off(duration=duration) except yeelight.BulbException as ex: _LOGGER.error("Unable to turn the bulb off: %s", ex) + + def set_mode(self, mode: str): + """Set a power mode.""" + import yeelight + try: + self._bulb.set_power_mode(yeelight.enums.PowerMode[mode.upper()]) + except yeelight.BulbException as ex: + _LOGGER.error("Unable to set the power mode: %s", ex) From 2bebfec3a62824c5d86cf8c2dc1da5ac1a7085df Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Mar 2018 23:39:25 +0200 Subject: [PATCH 038/136] Homekit: Fix security systems (#13499) * Fix alarm_code=None * Added test --- .../components/homekit/type_security_systems.py | 4 +++- .../homekit/test_type_security_systems.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 146fca95b53..b23522f0ea2 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -58,7 +58,9 @@ class SecuritySystem(HomeAccessory): hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] - params = {ATTR_ENTITY_ID: self._entity_id, ATTR_CODE: self._alarm_code} + params = {ATTR_ENTITY_ID: self._entity_id} + if self._alarm_code: + params[ATTR_CODE] = self._alarm_code self._hass.services.call('alarm_control_panel', service, params) def update_state(self, entity_id=None, old_state=None, new_state=None): diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 4d61fc4a44c..c689a73bac2 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -102,3 +102,19 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 3) + + def test_no_alarm_code(self): + """Test accessory if security_system doesn't require a alarm_code.""" + acp = 'alarm_control_panel.test' + + acc = SecuritySystem(self.hass, acp, 'SecuritySystem', + alarm_code=None, aid=2) + acc.run() + + # Set from HomeKit + acc.char_target_state.set_value(0) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') + self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) + self.assertEqual(acc.char_target_state.value, 0) From 00c6df54b27b0d7be47463ef2752f99c259ab98d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 28 Mar 2018 08:27:56 +0200 Subject: [PATCH 039/136] Upgrade slacker to 0.9.65 (#13496) --- homeassistant/components/notify/slack.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 30aadfc8297..b50260e4c61 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -17,7 +17,7 @@ from homeassistant.components.notify import ( BaseNotificationService) from homeassistant.const import (CONF_API_KEY, CONF_USERNAME, CONF_ICON) -REQUIREMENTS = ['slacker==0.9.60'] +REQUIREMENTS = ['slacker==0.9.65'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 026892099f1..d57ecd7f93a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1136,7 +1136,7 @@ simplisafe-python==1.0.5 skybellpy==0.1.1 # homeassistant.components.notify.slack -slacker==0.9.60 +slacker==0.9.65 # homeassistant.components.notify.xmpp sleekxmpp==1.3.2 From bdb4d754ae47073c7298d2a4288e7d0303677fe8 Mon Sep 17 00:00:00 2001 From: Mikael Svensson Date: Wed, 28 Mar 2018 09:04:18 +0200 Subject: [PATCH 040/136] Adds template function state_attr to get attribute from a state (#13378) * Adds template function state_attr to get attribute from a state Refactored is_state_attr to use new function Adds tests for state_attr * Fixes line too long and test bug * Fixes pylint error * Fixes tests and D401 lint error --- homeassistant/helpers/template.py | 13 ++++++++++--- tests/helpers/test_template.py | 13 +++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 28ab4e9bfa0..a04023cfc4f 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -28,7 +28,7 @@ DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S" _RE_NONE_ENTITIES = re.compile(r"distance\(|closest\(", re.I | re.M) _RE_GET_ENTITIES = re.compile( - r"(?:(?:states\.|(?:is_state|is_state_attr|states)" + r"(?:(?:states\.|(?:is_state|is_state_attr|state_attr|states)" r"\((?:[\ \'\"]?))([\w]+\.[\w]+)|([\w]+))", re.I | re.M ) @@ -182,6 +182,7 @@ class Template(object): 'distance': template_methods.distance, 'is_state': self.hass.states.is_state, 'is_state_attr': template_methods.is_state_attr, + 'state_attr': template_methods.state_attr, 'states': AllStates(self.hass), }) @@ -405,9 +406,15 @@ class TemplateMethods(object): def is_state_attr(self, entity_id, name, value): """Test if a state is a specific attribute.""" + state_attr = self.state_attr(entity_id, name) + return state_attr is not None and state_attr == value + + def state_attr(self, entity_id, name): + """Get a specific attribute from a state.""" state_obj = self._hass.states.get(entity_id) - return state_obj is not None and \ - state_obj.attributes.get(name) == value + if state_obj is not None: + return state_obj.attributes.get(name) + return None def _resolve_state(self, entity_id_or_state): """Return state or entity_id if given.""" diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index def06ea9284..693c3909924 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -397,6 +397,19 @@ class TestHelpersTemplate(unittest.TestCase): """, self.hass) self.assertEqual('False', tpl.render()) + def test_state_attr(self): + """Test state_attr method.""" + self.hass.states.set('test.object', 'available', {'mode': 'on'}) + tpl = template.Template(""" +{% if state_attr("test.object", "mode") == "on" %}yes{% else %}no{% endif %} + """, self.hass) + self.assertEqual('yes', tpl.render()) + + tpl = template.Template(""" +{{ state_attr("test.noobject", "mode") == None }} + """, self.hass) + self.assertEqual('True', tpl.render()) + def test_states_function(self): """Test using states as a function.""" self.hass.states.set('test.object', 'available') From 45ff15bc85b47814343cabb725fd76c373091435 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 28 Mar 2018 12:45:24 +0200 Subject: [PATCH 041/136] Upgrade aiohttp to 3.1.1 (#13510) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 317c1c8bc6c..85f8d5dcf12 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.1.0 +aiohttp==3.1.1 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 diff --git a/requirements_all.txt b/requirements_all.txt index d57ecd7f93a..10e6050bd4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.1.0 +aiohttp==3.1.1 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 diff --git a/setup.py b/setup.py index 9324713e71e..db4b1f8df92 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ REQUIRES = [ 'jinja2>=2.10', 'voluptuous==0.11.1', 'typing>=3,<4', - 'aiohttp==3.1.0', + 'aiohttp==3.1.1', 'async_timeout==2.0.1', 'astral==1.6', 'certifi>=2017.4.17', From b3b7cf3fa7ac744a976a42b1ac3b642dadc86b11 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Wed, 28 Mar 2018 23:50:09 +0100 Subject: [PATCH 042/136] Update tradfri v5 (#11187) * First pass to support simplified colour management in tradfri * Fix lint * Fix lint * Update imports * Prioritise brightness for transition * Fix bug * None check * Bracket * Import * Fix bugs * Change colour logic * Denormalise colour * Lint * Fix bug * Fix bugs, expose rgb conversion * Fix bug * Fix bug * Fix bug * Improve XY * Improve XY * async/wait for tradfri. * Bump requirement * Formatting. * Remove comma * Line length, shadowing * Switch to new HS colour system, using native data from tradfri gateway. * Lint. * Brightness bug. * Remove guard. * Temp workaround for bug. * Temp workaround for bug. * Temp workaround for bug. * Safety. * Switch logic. * Integrate latest * Fixes. * Fixes. * Mired validation. * Set bounds. * Transition time. * Transition time. * Transition time. * Fix brightness values. --- homeassistant/components/light/tradfri.py | 175 +++++++++------------ homeassistant/components/sensor/tradfri.py | 16 +- homeassistant/components/tradfri.py | 43 +++-- requirements_all.txt | 2 +- 4 files changed, 100 insertions(+), 136 deletions(-) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 1851579a172..ca153042f8d 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -4,11 +4,9 @@ Support for the IKEA Tradfri platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.tradfri/ """ -import asyncio import logging from homeassistant.core import callback -from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, @@ -17,20 +15,19 @@ from homeassistant.components.light import \ PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \ KEY_API -from homeassistant.util import color as color_util _LOGGER = logging.getLogger(__name__) +ATTR_TRANSITION_TIME = 'transition_time' DEPENDENCIES = ['tradfri'] PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA IKEA = 'IKEA of Sweden' TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager' SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) -ALLOWED_TEMPERATURES = {IKEA} -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, + async_add_devices, discovery_info=None): """Set up the IKEA Tradfri Light platform.""" if discovery_info is None: return @@ -40,8 +37,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): gateway = hass.data[KEY_GATEWAY][gateway_id] devices_command = gateway.get_devices() - devices_commands = yield from api(devices_command) - devices = yield from api(devices_commands) + devices_commands = await api(devices_command) + devices = await api(devices_commands) lights = [dev for dev in devices if dev.has_light_control] if lights: async_add_devices(TradfriLight(light, api) for light in lights) @@ -49,8 +46,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id] if allow_tradfri_groups: groups_command = gateway.get_groups() - groups_commands = yield from api(groups_command) - groups = yield from api(groups_commands) + groups_commands = await api(groups_command) + groups = await api(groups_commands) if groups: async_add_devices(TradfriGroup(group, api) for group in groups) @@ -66,8 +63,7 @@ class TradfriGroup(Light): self._refresh(light) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Start thread when added to hass.""" self._async_start_observe() @@ -96,13 +92,11 @@ class TradfriGroup(Light): """Return the brightness of the group lights.""" return self._group.dimmer - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Instruct the group lights to turn off.""" - yield from self._api(self._group.set_state(0)) + await self._api(self._group.set_state(0)) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Instruct the group lights to turn on, or dim.""" keys = {} if ATTR_TRANSITION in kwargs: @@ -112,16 +106,16 @@ class TradfriGroup(Light): if kwargs[ATTR_BRIGHTNESS] == 255: kwargs[ATTR_BRIGHTNESS] = 254 - yield from self._api( + await self._api( self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) else: - yield from self._api(self._group.set_state(1)) + await self._api(self._group.set_state(1)) @callback def _async_start_observe(self, exc=None): """Start observation of light.""" # pylint: disable=import-error - from pytradfri.error import PyTradFriError + from pytradfri.error import PytradfriError if exc: _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) @@ -131,7 +125,7 @@ class TradfriGroup(Light): err_callback=self._async_start_observe, duration=0) self.hass.async_add_job(self._api(cmd)) - except PyTradFriError as err: + except PytradfriError as err: _LOGGER.warning("Observation failed, trying again", exc_info=err) self._async_start_observe() @@ -159,7 +153,6 @@ class TradfriLight(Light): self._name = None self._hs_color = None self._features = SUPPORTED_FEATURES - self._temp_supported = False self._available = True self._refresh(light) @@ -167,33 +160,14 @@ class TradfriLight(Light): @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" - if self._light_control.max_kelvin is not None: - return color_util.color_temperature_kelvin_to_mired( - self._light_control.max_kelvin - ) + return self._light_control.min_mireds @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" - if self._light_control.min_kelvin is not None: - return color_util.color_temperature_kelvin_to_mired( - self._light_control.min_kelvin - ) + return self._light_control.max_mireds - @property - def device_state_attributes(self): - """Return the devices' state attributes.""" - info = self._light.device_info - - attrs = {} - - if info.battery_level is not None: - attrs[ATTR_BATTERY_LEVEL] = info.battery_level - - return attrs - - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Start thread when added to hass.""" self._async_start_observe() @@ -229,64 +203,73 @@ class TradfriLight(Light): @property def color_temp(self): - """Return the CT color value in mireds.""" - kelvin_color = self._light_data.kelvin_color_inferred - if kelvin_color is not None: - return color_util.color_temperature_kelvin_to_mired( - kelvin_color - ) + """Return the color temp value in mireds.""" + return self._light_data.color_temp @property def hs_color(self): """HS color of the light.""" - return self._hs_color + if self._light_control.can_set_color: + hsbxy = self._light_data.hsb_xy_color + hue = hsbxy[0] / (65535 / 360) + sat = hsbxy[1] / (65279 / 100) + if hue is not None and sat is not None: + return hue, sat - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Instruct the light to turn off.""" - yield from self._api(self._light_control.set_state(False)) + await self._api(self._light_control.set_state(False)) - @asyncio.coroutine - def async_turn_on(self, **kwargs): - """ - Instruct the light to turn on. - - After adding "self._light_data.hexcolor is not None" - for ATTR_HS_COLOR, this also supports Philips Hue bulbs. - """ - if ATTR_HS_COLOR in kwargs and self._light_data.hex_color is not None: - rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) - yield from self._api( - self._light.light_control.set_rgb_color(*rgb)) - - elif ATTR_COLOR_TEMP in kwargs and \ - self._light_data.hex_color is not None and \ - self._temp_supported: - kelvin = color_util.color_temperature_mired_to_kelvin( - kwargs[ATTR_COLOR_TEMP]) - yield from self._api( - self._light_control.set_kelvin_color(kelvin)) - - keys = {} + async def async_turn_on(self, **kwargs): + """Instruct the light to turn on.""" + params = {} + transition_time = None if ATTR_TRANSITION in kwargs: - keys['transition_time'] = int(kwargs[ATTR_TRANSITION]) * 10 + transition_time = int(kwargs[ATTR_TRANSITION]) * 10 - if ATTR_BRIGHTNESS in kwargs: - if kwargs[ATTR_BRIGHTNESS] == 255: - kwargs[ATTR_BRIGHTNESS] = 254 + brightness = kwargs.get(ATTR_BRIGHTNESS) - yield from self._api( - self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS], - **keys)) + if brightness is not None: + if brightness > 254: + brightness = 254 + elif brightness < 0: + brightness = 0 + + if ATTR_HS_COLOR in kwargs and self._light_control.can_set_color: + params[ATTR_BRIGHTNESS] = brightness + hue = int(kwargs[ATTR_HS_COLOR][0] * (65535 / 360)) + sat = int(kwargs[ATTR_HS_COLOR][1] * (65279 / 100)) + await self._api( + self._light_control.set_hsb(hue, sat, **params)) + return + + if ATTR_COLOR_TEMP in kwargs and self._light_control.can_set_temp: + temp = kwargs[ATTR_COLOR_TEMP] + if temp > self.max_mireds: + temp = self.max_mireds + elif temp < self.min_mireds: + temp = self.min_mireds + + if brightness is None: + params[ATTR_TRANSITION_TIME] = transition_time + await self._api( + self._light_control.set_color_temp(temp, + **params)) + + if brightness is not None: + params[ATTR_TRANSITION_TIME] = transition_time + await self._api( + self._light_control.set_dimmer(brightness, + **params)) else: - yield from self._api( + await self._api( self._light_control.set_state(True)) @callback def _async_start_observe(self, exc=None): """Start observation of light.""" # pylint: disable=import-error - from pytradfri.error import PyTradFriError + from pytradfri.error import PytradfriError if exc: _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) @@ -296,7 +279,7 @@ class TradfriLight(Light): err_callback=self._async_start_observe, duration=0) self.hass.async_add_job(self._api(cmd)) - except PyTradFriError as err: + except PytradfriError as err: _LOGGER.warning("Observation failed, trying again", exc_info=err) self._async_start_observe() @@ -309,27 +292,15 @@ class TradfriLight(Light): self._light_control = light.light_control self._light_data = light.light_control.lights[0] self._name = light.name - self._hs_color = None self._features = SUPPORTED_FEATURES - if self._light.device_info.manufacturer == IKEA: - if self._light_control.can_set_kelvin: - self._features |= SUPPORT_COLOR_TEMP - if self._light_control.can_set_color: - self._features |= SUPPORT_COLOR - else: - if self._light_data.hex_color is not None: - self._features |= SUPPORT_COLOR - - self._temp_supported = self._light.device_info.manufacturer \ - in ALLOWED_TEMPERATURES + if light.light_control.can_set_color: + self._features |= SUPPORT_COLOR + if light.light_control.can_set_temp: + self._features |= SUPPORT_COLOR_TEMP @callback def _observe_update(self, tradfri_device): """Receive new state data for this light.""" self._refresh(tradfri_device) - rgb = color_util.rgb_hex_to_rgb_list( - self._light_data.hex_color_inferred - ) - self._hs_color = color_util.color_RGB_to_hs(*rgb) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/sensor/tradfri.py b/homeassistant/components/sensor/tradfri.py index d087fdda9f6..df931770cf2 100644 --- a/homeassistant/components/sensor/tradfri.py +++ b/homeassistant/components/sensor/tradfri.py @@ -4,7 +4,6 @@ Support for the IKEA Tradfri platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.tradfri/ """ -import asyncio import logging from datetime import timedelta @@ -20,8 +19,8 @@ DEPENDENCIES = ['tradfri'] SCAN_INTERVAL = timedelta(minutes=5) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the IKEA Tradfri device platform.""" if discovery_info is None: return @@ -31,8 +30,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): gateway = hass.data[KEY_GATEWAY][gateway_id] devices_command = gateway.get_devices() - devices_commands = yield from api(devices_command) - all_devices = yield from api(devices_commands) + devices_commands = await api(devices_command) + all_devices = await api(devices_commands) devices = [dev for dev in all_devices if not dev.has_light_control] async_add_devices(TradfriDevice(device, api) for device in devices) @@ -48,8 +47,7 @@ class TradfriDevice(Entity): self._refresh(device) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Start thread when added to hass.""" self._async_start_observe() @@ -91,7 +89,7 @@ class TradfriDevice(Entity): def _async_start_observe(self, exc=None): """Start observation of light.""" # pylint: disable=import-error - from pytradfri.error import PyTradFriError + from pytradfri.error import PytradfriError if exc: _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) @@ -101,7 +99,7 @@ class TradfriDevice(Entity): err_callback=self._async_start_observe, duration=0) self.hass.async_add_job(self._api(cmd)) - except PyTradFriError as err: + except PytradfriError as err: _LOGGER.warning("Observation failed, trying again", exc_info=err) self._async_start_observe() diff --git a/homeassistant/components/tradfri.py b/homeassistant/components/tradfri.py index 5ac4d2a4eb1..72d1b4c769f 100644 --- a/homeassistant/components/tradfri.py +++ b/homeassistant/components/tradfri.py @@ -1,10 +1,9 @@ """ -Support for Ikea Tradfri. +Support for IKEA Tradfri. For more details about this component, please refer to the documentation at https://home-assistant.io/components/ikea_tradfri/ """ -import asyncio import logging from uuid import uuid4 @@ -16,7 +15,7 @@ from homeassistant.const import CONF_HOST from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pytradfri[async]==4.1.0'] +REQUIREMENTS = ['pytradfri[async]==5.4.2'] DOMAIN = 'tradfri' GATEWAY_IDENTITY = 'homeassistant' @@ -49,8 +48,7 @@ def request_configuration(hass, config, host): if instance: return - @asyncio.coroutine - def configuration_callback(callback_data): + async def configuration_callback(callback_data): """Handle the submitted configuration.""" try: from pytradfri.api.aiocoap_api import APIFactory @@ -67,14 +65,14 @@ def request_configuration(hass, config, host): # pytradfri aiocoap API into an endless loop. # Should just raise a requestError or something. try: - key = yield from api_factory.generate_psk(security_code) + key = await api_factory.generate_psk(security_code) except RequestError: configurator.async_notify_errors(hass, instance, "Security Code not accepted.") return - res = yield from _setup_gateway(hass, config, host, identity, key, - DEFAULT_ALLOW_TRADFRI_GROUPS) + res = await _setup_gateway(hass, config, host, identity, key, + DEFAULT_ALLOW_TRADFRI_GROUPS) if not res: configurator.async_notify_errors(hass, instance, @@ -101,18 +99,16 @@ def request_configuration(hass, config, host): ) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Tradfri component.""" conf = config.get(DOMAIN, {}) host = conf.get(CONF_HOST) allow_tradfri_groups = conf.get(CONF_ALLOW_TRADFRI_GROUPS) - known_hosts = yield from hass.async_add_job(load_json, - hass.config.path(CONFIG_FILE)) + known_hosts = await hass.async_add_job(load_json, + hass.config.path(CONFIG_FILE)) - @asyncio.coroutine - def gateway_discovered(service, info, - allow_tradfri_groups=DEFAULT_ALLOW_TRADFRI_GROUPS): + async def gateway_discovered(service, info, + allow_groups=DEFAULT_ALLOW_TRADFRI_GROUPS): """Run when a gateway is discovered.""" host = info['host'] @@ -121,23 +117,22 @@ def async_setup(hass, config): # identity was hard coded as 'homeassistant' identity = known_hosts[host].get('identity', 'homeassistant') key = known_hosts[host].get('key') - yield from _setup_gateway(hass, config, host, identity, key, - allow_tradfri_groups) + await _setup_gateway(hass, config, host, identity, key, + allow_groups) else: hass.async_add_job(request_configuration, hass, config, host) discovery.async_listen(hass, SERVICE_IKEA_TRADFRI, gateway_discovered) if host: - yield from gateway_discovered(None, - {'host': host}, - allow_tradfri_groups) + await gateway_discovered(None, + {'host': host}, + allow_tradfri_groups) return True -@asyncio.coroutine -def _setup_gateway(hass, hass_config, host, identity, key, - allow_tradfri_groups): +async def _setup_gateway(hass, hass_config, host, identity, key, + allow_tradfri_groups): """Create a gateway.""" from pytradfri import Gateway, RequestError # pylint: disable=import-error try: @@ -151,7 +146,7 @@ def _setup_gateway(hass, hass_config, host, identity, key, loop=hass.loop) api = factory.request gateway = Gateway() - gateway_info_result = yield from api(gateway.get_gateway_info()) + gateway_info_result = await api(gateway.get_gateway_info()) except RequestError: _LOGGER.exception("Tradfri setup failed.") return False diff --git a/requirements_all.txt b/requirements_all.txt index 10e6050bd4b..15f6c1ddbda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1021,7 +1021,7 @@ pytouchline==0.7 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==4.1.0 +pytradfri[async]==5.4.2 # homeassistant.components.device_tracker.unifi pyunifi==2.13 From ef7fd9f380c0f1e7fb3c61e54e1abfb601dcbcde Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 29 Mar 2018 00:55:05 +0200 Subject: [PATCH 043/136] python-miio version bumped (Closes: 13449) (#13511) --- homeassistant/components/fan/xiaomi_miio.py | 2 +- homeassistant/components/light/xiaomi_miio.py | 2 +- homeassistant/components/remote/xiaomi_miio.py | 2 +- homeassistant/components/sensor/xiaomi_miio.py | 2 +- homeassistant/components/switch/xiaomi_miio.py | 2 +- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- requirements_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index a1cb0431381..0eb0823a116 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -49,7 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'zhimi.humidifier.ca1']), }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] ATTR_MODEL = 'model' diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index a21c86f49c0..999d0f7f0e6 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.candle2']), }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] # The light does not accept cct values < 1 CCT_MIN = 1 diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 91f753391fc..13ec9c873b1 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index cb172735ac4..33ba5793fe0 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] ATTR_POWER = 'power' ATTR_CHARGING = 'charging' diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 9f0f163df69..27c3c4c72f1 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -37,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.v2']), }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index f42a895f94f..887a50fdcce 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 15f6c1ddbda..b07ed3d8fac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -948,7 +948,7 @@ python-juicenet==0.0.5 # homeassistant.components.sensor.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio -python-miio==0.3.8 +python-miio==0.3.9 # homeassistant.components.media_player.mpd python-mpd2==0.5.5 From 3b537f6e2afc2792b5f12f97b6e89e5371aa396c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 29 Mar 2018 10:40:41 +0200 Subject: [PATCH 044/136] Fix typos and update link (fixes #13520) (#13529) --- .github/ISSUE_TEMPLATE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 84464220749..8772a136eb3 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,9 +1,9 @@ **Home Assistant release with the issue:** @@ -23,7 +23,7 @@ Please provide details about your environment. **Component/platform:** From cea2de5eb5dacc8b398b5437425472e4c5e2e978 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Mar 2018 18:35:57 +0200 Subject: [PATCH 045/136] HomeKit: Fix setting light brightness (#13518) * Added test --- .../components/homekit/type_lights.py | 21 +++++--- tests/components/homekit/test_type_lights.py | 50 ++++++++++++------- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 2415bb1a4df..d88e7100131 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -32,6 +32,7 @@ class Light(HomeAccessory): self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: False} + self._state = 0 self.chars = [] self._features = self._hass.states.get(self._entity_id) \ @@ -47,7 +48,7 @@ class Light(HomeAccessory): serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) self.char_on = serv_light.get_characteristic(CHAR_ON) self.char_on.setter_callback = self.set_state - self.char_on.value = 0 + self.char_on.value = self._state if CHAR_BRIGHTNESS in self.chars: self.char_brightness = serv_light \ @@ -66,7 +67,7 @@ class Light(HomeAccessory): def set_state(self, value): """Set state if call came from HomeKit.""" - if self._flag[CHAR_BRIGHTNESS]: + if self._state == value: return _LOGGER.debug('%s: Set state to %d', self._entity_id, value) @@ -83,8 +84,11 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) self._flag[CHAR_BRIGHTNESS] = True self.char_brightness.set_value(value, should_callback=False) - self._hass.components.light.turn_on( - self._entity_id, brightness_pct=value) + if value != 0: + self._hass.components.light.turn_on( + self._entity_id, brightness_pct=value) + else: + self._hass.components.light.turn_off(self._entity_id) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" @@ -121,10 +125,11 @@ class Light(HomeAccessory): # Handle State state = new_state.state - if not self._flag[CHAR_ON] and state in [STATE_ON, STATE_OFF] and \ - self.char_on.value != (state == STATE_ON): - self.char_on.set_value(state == STATE_ON, should_callback=False) - self._flag[CHAR_ON] = False + if state in (STATE_ON, STATE_OFF): + self._state = 1 if state == STATE_ON else 0 + if not self._flag[CHAR_ON] and self.char_on.value != self._state: + self.char_on.set_value(self._state, should_callback=False) + self._flag[CHAR_ON] = False # Handle Brightness if CHAR_BRIGHTNESS in self.chars: diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index b4d4d5a5945..ee1900fd7c5 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -57,19 +57,21 @@ class TestHomekitLights(unittest.TestCase): self.assertEqual(acc.char_on.value, 0) # Set from HomeKit - acc.char_on.set_value(True) + acc.char_on.set_value(1) self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - acc.char_on.set_value(False) + self.hass.states.set(entity_id, STATE_ON) + self.hass.block_till_done() + + acc.char_on.set_value(0) + self.hass.block_till_done() + self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) + + self.hass.states.set(entity_id, STATE_OFF) self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) # Remove entity self.hass.states.remove(entity_id) @@ -95,15 +97,27 @@ class TestHomekitLights(unittest.TestCase): acc.char_brightness.set_value(20) acc.char_on.set_value(1) self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - print(self.events[0].data) + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA], { ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20}) + acc.char_on.set_value(1) + acc.char_brightness.set_value(40) + self.hass.block_till_done() + self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual( + self.events[1].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 40}) + + acc.char_on.set_value(1) + acc.char_brightness.set_value(0) + self.hass.block_till_done() + self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF) + def test_light_rgb_color(self): """Test light with rgb_color.""" entity_id = 'light.demo' @@ -123,10 +137,8 @@ class TestHomekitLights(unittest.TestCase): acc.char_hue.set_value(145) acc.char_saturation.set_value(75) self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA], { ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (145, 75)}) From 298e6eeef189fb36ccb0a381af1695c36531cdfc Mon Sep 17 00:00:00 2001 From: NovapaX Date: Thu, 29 Mar 2018 21:39:56 +0200 Subject: [PATCH 046/136] Tradfri - unique_id's and color_temp support for rgb-bulbs (#13531) * unique_ids for tradfri lights and groups * set color temperature on CWS bulb Cannot set_color_temp on color bulb, needs conversion from mired to hsb * make travis happy * change condition so we ensure color bulbs are included, change comments. --- homeassistant/components/light/tradfri.py | 51 ++++++++++++++++++----- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index ca153042f8d..227ed419aec 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -15,6 +15,7 @@ from homeassistant.components.light import \ PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \ KEY_API +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -41,7 +42,8 @@ async def async_setup_platform(hass, config, devices = await api(devices_commands) lights = [dev for dev in devices if dev.has_light_control] if lights: - async_add_devices(TradfriLight(light, api) for light in lights) + async_add_devices( + TradfriLight(light, api, gateway_id) for light in lights) allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id] if allow_tradfri_groups: @@ -49,24 +51,31 @@ async def async_setup_platform(hass, config, groups_commands = await api(groups_command) groups = await api(groups_commands) if groups: - async_add_devices(TradfriGroup(group, api) for group in groups) + async_add_devices( + TradfriGroup(group, api, gateway_id) for group in groups) class TradfriGroup(Light): """The platform class required by hass.""" - def __init__(self, light, api): + def __init__(self, group, api, gateway_id): """Initialize a Group.""" self._api = api - self._group = light - self._name = light.name + self._unique_id = "group-{}-{}".format(gateway_id, group.id) + self._group = group + self._name = group.name - self._refresh(light) + self._refresh(group) async def async_added_to_hass(self): """Start thread when added to hass.""" self._async_start_observe() + @property + def unique_id(self): + """Return unique ID for this group.""" + return self._unique_id + @property def should_poll(self): """No polling needed for tradfri group.""" @@ -144,9 +153,10 @@ class TradfriGroup(Light): class TradfriLight(Light): """The platform class required by Home Assistant.""" - def __init__(self, light, api): + def __init__(self, light, api, gateway_id): """Initialize a Light.""" self._api = api + self._unique_id = "light-{}-{}".format(gateway_id, light.id) self._light = None self._light_control = None self._light_data = None @@ -157,6 +167,11 @@ class TradfriLight(Light): self._refresh(light) + @property + def unique_id(self): + """Return unique ID for light.""" + return self._unique_id + @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" @@ -243,7 +258,8 @@ class TradfriLight(Light): self._light_control.set_hsb(hue, sat, **params)) return - if ATTR_COLOR_TEMP in kwargs and self._light_control.can_set_temp: + if ATTR_COLOR_TEMP in kwargs and (self._light_control.can_set_temp or + self._light_control.can_set_color): temp = kwargs[ATTR_COLOR_TEMP] if temp > self.max_mireds: temp = self.max_mireds @@ -252,9 +268,22 @@ class TradfriLight(Light): if brightness is None: params[ATTR_TRANSITION_TIME] = transition_time - await self._api( - self._light_control.set_color_temp(temp, - **params)) + # White Spectrum bulb + if (self._light_control.can_set_temp and + not self._light_control.can_set_color): + await self._api( + self._light_control.set_color_temp(temp, **params)) + # Color bulb (CWS) + # color_temp needs to be set with hue/saturation + if self._light_control.can_set_color: + params[ATTR_BRIGHTNESS] = brightness + temp_k = color_util.color_temperature_mired_to_kelvin(temp) + hs_color = color_util.color_temperature_to_hs(temp_k) + hue = int(hs_color[0] * (65535 / 360)) + sat = int(hs_color[1] * (65279 / 100)) + await self._api( + self._light_control.set_hsb(hue, sat, + **params)) if brightness is not None: params[ATTR_TRANSITION_TIME] = transition_time From 3fdb0002a79abfe75f9207d7c01aff472d25bfb5 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Thu, 29 Mar 2018 23:29:46 +0200 Subject: [PATCH 047/136] Qwikswitch async refactor & sensor (#13509) --- homeassistant/components/light/qwikswitch.py | 32 +++- homeassistant/components/qwikswitch.py | 171 ++++++++---------- homeassistant/components/sensor/qwikswitch.py | 69 +++++++ homeassistant/components/switch/qwikswitch.py | 22 ++- requirements_all.txt | 2 +- 5 files changed, 185 insertions(+), 111 deletions(-) create mode 100644 homeassistant/components/sensor/qwikswitch.py diff --git a/homeassistant/components/light/qwikswitch.py b/homeassistant/components/light/qwikswitch.py index c4faf0f9ca0..26741525b8f 100644 --- a/homeassistant/components/light/qwikswitch.py +++ b/homeassistant/components/light/qwikswitch.py @@ -4,18 +4,32 @@ Support for Qwikswitch Relays and Dimmers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.qwikswitch/ """ -import logging +from homeassistant.components.qwikswitch import ( + QSToggleEntity, DOMAIN as QWIKSWITCH) +from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light -DEPENDENCIES = ['qwikswitch'] +DEPENDENCIES = [QWIKSWITCH] -# pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, _, add_devices, discovery_info=None): """Add lights from the main Qwikswitch component.""" if discovery_info is None: - logging.getLogger(__name__).error( - "Configure Qwikswitch Light component failed") - return False + return - add_devices(hass.data['qwikswitch']['light']) - return True + qsusb = hass.data[QWIKSWITCH] + devs = [QSLight(qsid, qsusb) for qsid in discovery_info[QWIKSWITCH]] + add_devices(devs) + + +class QSLight(QSToggleEntity, Light): + """Light based on a Qwikswitch relay/dimmer module.""" + + @property + def brightness(self): + """Return the brightness of this light (0-255).""" + return self._qsusb[self.qsid, 1] if self._dim else None + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS if self._dim else 0 diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index c4901805e3e..708eff7cf11 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -4,21 +4,21 @@ Support for Qwikswitch devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/qwikswitch/ """ -import asyncio import logging import voluptuous as vol from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL) + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL, + CONF_SENSORS, CONF_SWITCHES) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import load_platform -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) -from homeassistant.components.switch import SwitchDevice +from homeassistant.helpers.entity import Entity +from homeassistant.components.light import ATTR_BRIGHTNESS +import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyqwikswitch==0.5'] +REQUIREMENTS = ['pyqwikswitch==0.6'] _LOGGER = logging.getLogger(__name__) @@ -33,11 +33,14 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_URL, default='http://127.0.0.1:2020'): vol.Coerce(str), vol.Optional(CONF_DIMMER_ADJUST, default=1): CV_DIM_VALUE, - vol.Optional(CONF_BUTTON_EVENTS): vol.Coerce(str) + vol.Optional(CONF_BUTTON_EVENTS, default=[]): cv.ensure_list_csv, + vol.Optional(CONF_SENSORS, default={}): vol.Schema({cv.slug: str}), + vol.Optional(CONF_SWITCHES, default=[]): vol.All( + cv.ensure_list, [str]) })}, extra=vol.ALLOW_EXTRA) -class QSToggleEntity(object): +class QSToggleEntity(Entity): """Representation of a Qwikswitch Entity. Implement base QS methods. Modeled around HA ToggleEntity[1] & should only @@ -55,7 +58,7 @@ class QSToggleEntity(object): def __init__(self, qsid, qsusb): """Initialize the ToggleEntity.""" from pyqwikswitch import (QS_NAME, QSDATA, QS_TYPE, QSType) - self._id = qsid + self.qsid = qsid self._qsusb = qsusb.devices dev = qsusb.devices[qsid] self._dim = dev[QS_TYPE] == QSType.dimmer @@ -74,129 +77,113 @@ class QSToggleEntity(object): @property def is_on(self): """Check if device is on (non-zero).""" - return self._qsusb[self._id, 1] > 0 + return self._qsusb[self.qsid, 1] > 0 - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" new = kwargs.get(ATTR_BRIGHTNESS, 255) - self._qsusb.set_value(self._id, new) + self._qsusb.set_value(self.qsid, new) - @asyncio.coroutine - def async_turn_on(self, **kwargs): - """Turn the device on.""" - new = kwargs.get(ATTR_BRIGHTNESS, 255) - self._qsusb.set_value(self._id, new) - - def turn_off(self, **kwargs): # pylint: disable=unused-argument + async def async_turn_off(self, **_): """Turn the device off.""" - self._qsusb.set_value(self._id, 0) + self._qsusb.set_value(self.qsid, 0) - @asyncio.coroutine - def async_turn_off(self, **kwargs): # pylint: disable=unused-argument - """Turn the device off.""" - self._qsusb.set_value(self._id, 0) + def _update(self, _packet=None): + """Schedule an update - match dispather_send signature.""" + self.async_schedule_update_ha_state() + + async def async_added_to_hass(self): + """Listen for updates from QSUSb via dispatcher.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + self.qsid, self._update) -class QSSwitch(QSToggleEntity, SwitchDevice): - """Switch based on a Qwikswitch relay module.""" - - pass - - -class QSLight(QSToggleEntity, Light): - """Light based on a Qwikswitch relay/dimmer module.""" - - @property - def brightness(self): - """Return the brightness of this light (0-255).""" - return self._qsusb[self._id, 1] if self._dim else None - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS if self._dim else None - - -@asyncio.coroutine -def async_setup(hass, config): - """Setup qwiskswitch component.""" - from pyqwikswitch.async import QSUsb +async def async_setup(hass, config): + """Qwiskswitch component setup.""" + from pyqwikswitch.async_ import QSUsb from pyqwikswitch import ( - CMD_BUTTONS, QS_CMD, QSDATA, QS_ID, QS_NAME, QS_TYPE, QSType) + CMD_BUTTONS, QS_CMD, QS_ID, QS_TYPE, QSType) - hass.data[DOMAIN] = {} - - # Override which cmd's in /&listen packets will fire events + # Add cmd's to in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] - cmd_buttons = config[DOMAIN].get(CONF_BUTTON_EVENTS, ','.join(CMD_BUTTONS)) - cmd_buttons = cmd_buttons.split(',') + cmd_buttons = set(CMD_BUTTONS) + for btn in config[DOMAIN][CONF_BUTTON_EVENTS]: + cmd_buttons.add(btn) url = config[DOMAIN][CONF_URL] dimmer_adjust = config[DOMAIN][CONF_DIMMER_ADJUST] + sensors = config[DOMAIN]['sensors'] + switches = config[DOMAIN]['switches'] - def callback_value_changed(qsdevices, key, new): \ - # pylint: disable=unused-argument - """Update entiry values based on device change.""" - entity = hass.data[DOMAIN].get(key) - if entity is not None: - entity.schedule_update_ha_state() # Part of Entity/ToggleEntity + def callback_value_changed(_qsd, qsid, _val): + """Update entity values based on device change.""" + _LOGGER.debug("Dispatch %s (update from devices)", qsid) + hass.helpers.dispatcher.async_dispatcher_send(qsid, None) session = async_get_clientsession(hass) qsusb = QSUsb(url=url, dim_adj=dimmer_adjust, session=session, callback_value_changed=callback_value_changed) - @callback - def async_stop(event): # pylint: disable=unused-argument - """Stop the listener queue and clean up.""" - nonlocal qsusb - qsusb.stop() - qsusb = None - hass.data[DOMAIN] = {} - _LOGGER.info("Waiting for long poll to QSUSB to time out") - - hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop) - # Discover all devices in QSUSB - yield from qsusb.update_from_devices() - hass.data[DOMAIN]['switch'] = [] - hass.data[DOMAIN]['light'] = [] + if not await qsusb.update_from_devices(): + return False + + hass.data[DOMAIN] = qsusb + + _new = {'switch': [], 'light': [], 'sensor': sensors} for _id, item in qsusb.devices: - if (item[QS_TYPE] == QSType.relay and - item[QSDATA][QS_NAME].lower().endswith(' switch')): - item[QSDATA][QS_NAME] = item[QSDATA][QS_NAME][:-7] # Remove switch - new_dev = QSSwitch(_id, qsusb) - hass.data[DOMAIN]['switch'].append(new_dev) + if _id in switches: + if item[QS_TYPE] != QSType.relay: + _LOGGER.warning( + "You specified a switch that is not a relay %s", _id) + continue + _new['switch'].append(_id) elif item[QS_TYPE] in [QSType.relay, QSType.dimmer]: - new_dev = QSLight(_id, qsusb) - hass.data[DOMAIN]['light'].append(new_dev) + _new['light'].append(_id) else: _LOGGER.warning("Ignored unknown QSUSB device: %s", item) continue - hass.data[DOMAIN][_id] = new_dev # Load platforms - for comp_name in ('switch', 'light'): - if hass.data[DOMAIN][comp_name]: - load_platform(hass, comp_name, 'qwikswitch', {}, config) + for comp_name, comp_conf in _new.items(): + if comp_conf: + load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config) def callback_qs_listen(item): """Typically a button press or update signal.""" - if qsusb is None: # Shutting down - return - # If button pressed, fire a hass event - if item.get(QS_CMD, '') in cmd_buttons and QS_ID in item: - hass.bus.async_fire('qwikswitch.button.{}'.format(item[QS_ID])) - return + if QS_ID in item: + if item.get(QS_CMD, '') in cmd_buttons: + hass.bus.async_fire( + 'qwikswitch.button.{}'.format(item[QS_ID]), item) + return + + # Private method due to bad __iter__ design in qsusb + # qsusb.devices returns a list of tuples + if item[QS_ID] not in \ + qsusb.devices._data: # pylint: disable=protected-access + # Not a standard device in, component can handle packet + # i.e. sensors + _LOGGER.debug("Dispatch %s ((%s))", item[QS_ID], item) + hass.helpers.dispatcher.async_dispatcher_send( + item[QS_ID], item) # Update all ha_objects hass.async_add_job(qsusb.update_from_devices) @callback - def async_start(event): # pylint: disable=unused-argument + def async_start(_): """Start listening.""" hass.async_add_job(qsusb.listen, callback_qs_listen) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start) + @callback + def async_stop(_): + """Stop the listener queue and clean up.""" + hass.data[DOMAIN].stop() + _LOGGER.info("Waiting for long poll to QSUSB to time out (max 30sec)") + + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop) + return True diff --git a/homeassistant/components/sensor/qwikswitch.py b/homeassistant/components/sensor/qwikswitch.py new file mode 100644 index 00000000000..19b32e93670 --- /dev/null +++ b/homeassistant/components/sensor/qwikswitch.py @@ -0,0 +1,69 @@ +""" +Support for Qwikswitch Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.qwikswitch/ +""" +import logging + +from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = [QWIKSWITCH] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, _, add_devices, discovery_info=None): + """Add lights from the main Qwikswitch component.""" + if discovery_info is None: + return + + qsusb = hass.data[QWIKSWITCH] + _LOGGER.debug("Setup qwikswitch.sensor %s, %s", qsusb, discovery_info) + devs = [QSSensor(name, qsid) + for name, qsid in discovery_info[QWIKSWITCH].items()] + add_devices(devs) + + +class QSSensor(Entity): + """Sensor based on a Qwikswitch relay/dimmer module.""" + + _val = {} + + def __init__(self, sensor_name, sensor_id): + """Initialize the sensor.""" + self._name = sensor_name + self.qsid = sensor_id + + def update_packet(self, packet): + """Receive update packet from QSUSB.""" + _LOGGER.debug("Update %s (%s): %s", self.entity_id, self.qsid, packet) + self._val = packet + self.async_schedule_update_ha_state() + + @property + def state(self): + """Return the value of the sensor.""" + return self._val.get('data', 0) + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return self._val + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return None + + @property + def poll(self): + """QS sensors gets packets in update_packet.""" + return False + + async def async_added_to_hass(self): + """Listen for updates from QSUSb via dispatcher.""" + # Part of Entity/ToggleEntity + self.hass.helpers.dispatcher.async_dispatcher_connect( + self.qsid, self.update_packet) diff --git a/homeassistant/components/switch/qwikswitch.py b/homeassistant/components/switch/qwikswitch.py index 258e1141052..193c2722534 100644 --- a/homeassistant/components/switch/qwikswitch.py +++ b/homeassistant/components/switch/qwikswitch.py @@ -4,18 +4,22 @@ Support for Qwikswitch relays. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.qwikswitch/ """ -import logging +from homeassistant.components.qwikswitch import ( + QSToggleEntity, DOMAIN as QWIKSWITCH) +from homeassistant.components.switch import SwitchDevice -DEPENDENCIES = ['qwikswitch'] +DEPENDENCIES = [QWIKSWITCH] -# pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, _, add_devices, discovery_info=None): """Add switches from the main Qwikswitch component.""" if discovery_info is None: - logging.getLogger(__name__).error( - "Configure Qwikswitch Switch component failed") - return False + return - add_devices(hass.data['qwikswitch']['switch']) - return True + qsusb = hass.data[QWIKSWITCH] + devs = [QSSwitch(qsid, qsusb) for qsid in discovery_info[QWIKSWITCH]] + add_devices(devs) + + +class QSSwitch(QSToggleEntity, SwitchDevice): + """Switch based on a Qwikswitch relay module.""" diff --git a/requirements_all.txt b/requirements_all.txt index b07ed3d8fac..5b204ff621d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -860,7 +860,7 @@ pyowm==2.8.0 pypollencom==1.1.1 # homeassistant.components.qwikswitch -pyqwikswitch==0.5 +pyqwikswitch==0.6 # homeassistant.components.rainbird pyrainbird==0.1.3 From f391cbae27ed6b7e23960acaa298fc58f63f7f85 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 29 Mar 2018 20:10:27 -0400 Subject: [PATCH 048/136] Fix Insteon Leak Sensor (#13515) * update leak sensor * Fix error when insteon state type is unknown * Bump insteon version to 0.8.3 * Update requirements all and test * Fix requirements conflicts due to lack of commit sync * Requirements sync * Rerun script/gen_requirements_all.py * Try requirements update again * Update requirements --- .../components/binary_sensor/insteon_plm.py | 16 +++++----- homeassistant/components/insteon_plm.py | 31 ++++++++++--------- requirements_all.txt | 2 +- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/binary_sensor/insteon_plm.py b/homeassistant/components/binary_sensor/insteon_plm.py index 09c4b5c8ea7..06079d6aa3b 100644 --- a/homeassistant/components/binary_sensor/insteon_plm.py +++ b/homeassistant/components/binary_sensor/insteon_plm.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = {'openClosedSensor': 'opening', 'motionSensor': 'motion', 'doorSensor': 'door', - 'leakSensor': 'moisture'} + 'wetLeakSensor': 'moisture'} @asyncio.coroutine @@ -28,13 +28,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): address = discovery_info['address'] device = plm.devices[address] state_key = discovery_info['state_key'] + name = device.states[state_key].name + if name != 'dryLeakSensor': + _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', + device.address.hex, device.states[state_key].name) - _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', - device.address.hex, device.states[state_key].name) + new_entity = InsteonPLMBinarySensor(device, state_key) - new_entity = InsteonPLMBinarySensor(device, state_key) - - async_add_devices([new_entity]) + async_add_devices([new_entity]) class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): @@ -53,5 +54,4 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): @property def is_on(self): """Return the boolean response if the node is on.""" - sensorstate = self._insteon_device_state.value - return bool(sensorstate) + return bool(self._insteon_device_state.value) diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py index 2381e3db69e..6f5c5223ea0 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.8.2'] +REQUIREMENTS = ['insteonplm==0.8.3'] _LOGGER = logging.getLogger(__name__) @@ -64,19 +64,20 @@ def async_setup(hass, config): """Detect device from transport to be delegated to platform.""" for state_key in device.states: platform_info = ipdb[device.states[state_key]] - platform = platform_info.platform - if platform is not None: - _LOGGER.info("New INSTEON PLM device: %s (%s) %s", - device.address, - device.states[state_key].name, - platform) + if platform_info: + platform = platform_info.platform + if platform: + _LOGGER.info("New INSTEON PLM device: %s (%s) %s", + device.address, + device.states[state_key].name, + platform) - hass.async_add_job( - discovery.async_load_platform( - hass, platform, DOMAIN, - discovered={'address': device.address.hex, - 'state_key': state_key}, - hass_config=config)) + hass.async_add_job( + discovery.async_load_platform( + hass, platform, DOMAIN, + discovered={'address': device.address.hex, + 'state_key': state_key}, + hass_config=config)) _LOGGER.info("Looking for PLM on %s", port) conn = yield from insteonplm.Connection.create( @@ -127,13 +128,15 @@ class IPDB(object): from insteonplm.states.sensor import (VariableSensor, OnOffSensor, SmokeCO2Sensor, - IoLincSensor) + IoLincSensor, + LeakSensorDryWet) self.states = [State(OnOffSwitch_OutletTop, 'switch'), State(OnOffSwitch_OutletBottom, 'switch'), State(OpenClosedRelay, 'switch'), State(OnOffSwitch, 'switch'), + State(LeakSensorDryWet, 'binary_sensor'), State(IoLincSensor, 'binary_sensor'), State(SmokeCO2Sensor, 'sensor'), State(OnOffSensor, 'binary_sensor'), diff --git a/requirements_all.txt b/requirements_all.txt index 5b204ff621d..1b3f8d163ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.8.2 +insteonplm==0.8.3 # homeassistant.components.verisure jsonpath==0.75 From 0a0b33af030ba7c448fac6b2293021efdadc883c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 30 Mar 2018 02:10:56 +0200 Subject: [PATCH 049/136] Fix mysensors light supported features (#13512) * Different types of light should have different supported features. --- homeassistant/components/light/mysensors.py | 29 ++++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 14a770b7632..7aa1e754c43 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -12,8 +12,7 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.util.color import rgb_hex_to_rgb_list import homeassistant.util.color as color_util -SUPPORT_MYSENSORS = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR | - SUPPORT_WHITE_VALUE) +SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE def setup_platform(hass, config, add_devices, discovery_info=None): @@ -64,11 +63,6 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): """Return true if device is on.""" return self._state - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_MYSENSORS - def _turn_on_light(self): """Turn on light child device.""" set_req = self.gateway.const.SetReq @@ -171,6 +165,11 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): class MySensorsLightDimmer(MySensorsLight): """Dimmer child class to MySensorsLight.""" + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() @@ -188,6 +187,14 @@ class MySensorsLightDimmer(MySensorsLight): class MySensorsLightRGB(MySensorsLight): """RGB child class to MySensorsLight.""" + @property + def supported_features(self): + """Flag supported features.""" + set_req = self.gateway.const.SetReq + if set_req.V_DIMMER in self._values: + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + return SUPPORT_COLOR + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() @@ -209,6 +216,14 @@ class MySensorsLightRGBW(MySensorsLightRGB): # pylint: disable=too-many-ancestors + @property + def supported_features(self): + """Flag supported features.""" + set_req = self.gateway.const.SetReq + if set_req.V_DIMMER in self._values: + return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW + return SUPPORT_MYSENSORS_RGBW + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() From ab9b9157312bb4dff4a407d640b2542737b86af0 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 30 Mar 2018 02:12:11 +0200 Subject: [PATCH 050/136] Construct version pinned (#13528) * Construct added to the requirements * requirements_all.txt updated --- homeassistant/components/climate/eq3btsmart.py | 2 +- homeassistant/components/fan/xiaomi_miio.py | 2 +- homeassistant/components/light/xiaomi_miio.py | 2 +- homeassistant/components/remote/xiaomi_miio.py | 2 +- .../components/sensor/eddystone_temperature.py | 2 +- homeassistant/components/sensor/xiaomi_miio.py | 2 +- homeassistant/components/switch/xiaomi_miio.py | 2 +- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- requirements_all.txt | 10 ++++++++++ 9 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 5c0a3530006..820e715b00d 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-eq3bt==0.1.9'] +REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 0eb0823a116..8dc6bb54bd1 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -49,7 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'zhimi.humidifier.ca1']), }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_MODEL = 'model' diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 999d0f7f0e6..21a27c33203 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.candle2']), }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] # The light does not accept cct values < 1 CCT_MIN = 1 diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 13ec9c873b1..b71eb2cb447 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py index fb5fa2c1fba..06accb26eb6 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_NAME, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) -REQUIREMENTS = ['beacontools[scan]==1.2.1'] +REQUIREMENTS = ['beacontools[scan]==1.2.1', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index 33ba5793fe0..066dc384007 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_CHARGING = 'charging' diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 27c3c4c72f1..6110b6dc469 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -37,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.v2']), }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 887a50fdcce..b2451ed495c 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1b3f8d163ca..8c355606194 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -189,6 +189,16 @@ colorlog==3.1.2 # homeassistant.components.binary_sensor.concord232 concord232==0.15 +# homeassistant.components.climate.eq3btsmart +# homeassistant.components.fan.xiaomi_miio +# homeassistant.components.light.xiaomi_miio +# homeassistant.components.remote.xiaomi_miio +# homeassistant.components.sensor.eddystone_temperature +# homeassistant.components.sensor.xiaomi_miio +# homeassistant.components.switch.xiaomi_miio +# homeassistant.components.vacuum.xiaomi_miio +construct==2.9.41 + # homeassistant.scripts.credstash # credstash==1.14.0 From a6b63b669e87494962d979f3a6d5f65d5db37bee Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 30 Mar 2018 02:13:08 +0200 Subject: [PATCH 051/136] Don't add Falsy items to list #13412 (#13536) --- homeassistant/config.py | 4 ++++ tests/test_config.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/config.py b/homeassistant/config.py index 53e611ac725..28936ae12e9 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -554,6 +554,8 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): continue if hasattr(component, 'PLATFORM_SCHEMA'): + if not comp_conf: + continue # Ensure we dont add Falsy items to list config[comp_name] = cv.ensure_list(config.get(comp_name)) config[comp_name].extend(cv.ensure_list(comp_conf)) continue @@ -562,6 +564,8 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): merge_type, _ = _identify_config_schema(component) if merge_type == 'list': + if not comp_conf: + continue # Ensure we dont add Falsy items to list config[comp_name] = cv.ensure_list(config.get(comp_name)) config[comp_name].extend(cv.ensure_list(comp_conf)) continue diff --git a/tests/test_config.py b/tests/test_config.py index 22fcebc6ea4..652b931366a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -592,6 +592,25 @@ def test_merge(merge_log_err): assert config['wake_on_lan'] is None +def test_merge_try_falsy(merge_log_err): + """Ensure we dont add falsy items like empty OrderedDict() to list.""" + packages = { + 'pack_falsy_to_lst': {'automation': OrderedDict()}, + 'pack_list2': {'light': OrderedDict()}, + } + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'automation': {'do': 'something'}, + 'light': {'some': 'light'}, + } + config_util.merge_packages_config(config, packages) + + assert merge_log_err.call_count == 0 + assert len(config) == 3 + assert len(config['automation']) == 1 + assert len(config['light']) == 1 + + def test_merge_new(merge_log_err): """Test adding new components to outer scope.""" packages = { From 5908b55bbabc5803833cc9ec88cbc17e8c2e48f1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Mar 2018 18:01:47 -0700 Subject: [PATCH 052/136] Fix merge conflict --- homeassistant/const.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0ac3899cba6..d286aa85458 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,13 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -<<<<<<< HEAD MINOR_VERSION = 67 PATCH_VERSION = '0.dev0' -======= -MINOR_VERSION = 66 -PATCH_VERSION = '0b3' ->>>>>>> origin/rc __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From df78eecc1b689bcba705cdd5ae60de5515f3251e Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 30 Mar 2018 02:10:20 +0100 Subject: [PATCH 053/136] Adds folder_watcher component (#12918) * Create watchdog_file_watcher.py * Rename watchdog_file_watcher.py to folder_watcher.py * Address a number of issues * Adds filter * Adds pattern matching * Adds create_event_handler() * Update folder_watcher.py * Adds run_setup() * Remove stop_watching() * Adds shutdown() * Update config to allow patterns on each folder * Update to patterns from filters * Adds watchdog * Fix indents on schema * Update folder_watcher.py * Create test_file_watcher.py * Fix lints * Add test_invalid_path() * Adds folder_watcher * Update test_file_watcher.py * Update folder_watcher.py * Simplify config * Adapt for new config * Run observer.schedule() on EVENT_HOMEASSISTANT_START * Amend Watcher removing entity and tidying startup * Tidy config * Rename process to on_any_event for consistency * Rename on_any_event back to process Using `on_any_event` resulted in 2 events being fired * Update folder_watcher.py * Fix return False on setup * Update test_file_watcher.py * Update folder_watcher.py * Adds watchdog * Undo adding watchdog * Update test_file_watcher.py * Update test_file_watcher.py * Update test_file_watcher.py * Update test_file_watcher.py * Update test_file_watcher.py * Add event * Update test_file_watcher.py * Update .coveragerc * Update test_file_watcher.py * Update test_file_watcher.py * debug + join * test event * lint * lint * Rename test_file_watcher.py to test_folder_watcher.py * hound * Tidy test * Further refine test * Adds to test_all * Fix test for py35 * Change test again * Update test_folder_watcher.py * Fix test * Add watchdog to test * Update folder_watcher.py * add watchdog * Update folder_watcher.py --- .coveragerc | 1 + homeassistant/components/folder_watcher.py | 111 +++++++++++++++++++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/test_folder_watcher.py | 64 ++++++++++++ 6 files changed, 183 insertions(+) create mode 100644 homeassistant/components/folder_watcher.py create mode 100644 tests/components/test_folder_watcher.py diff --git a/.coveragerc b/.coveragerc index a2c0dde77b1..d6cc126ef52 100644 --- a/.coveragerc +++ b/.coveragerc @@ -402,6 +402,7 @@ omit = homeassistant/components/fan/mqtt.py homeassistant/components/fan/xiaomi_miio.py homeassistant/components/feedreader.py + homeassistant/components/folder_watcher.py homeassistant/components/foursquare.py homeassistant/components/goalfeed.py homeassistant/components/ifttt.py diff --git a/homeassistant/components/folder_watcher.py b/homeassistant/components/folder_watcher.py new file mode 100644 index 00000000000..011ae892bc5 --- /dev/null +++ b/homeassistant/components/folder_watcher.py @@ -0,0 +1,111 @@ +""" +Component for monitoring activity on a folder. + +For more details about this platform, refer to the documentation at +https://home-assistant.io/components/folder_watcher/ +""" +import os +import logging +import voluptuous as vol +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['watchdog==0.8.3'] +_LOGGER = logging.getLogger(__name__) + +CONF_FOLDER = 'folder' +CONF_PATTERNS = 'patterns' +CONF_WATCHERS = 'watchers' +DEFAULT_PATTERN = '*' +DOMAIN = "folder_watcher" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_FOLDER): cv.isdir, + vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]): + vol.All(cv.ensure_list, [cv.string]), + })]) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the folder watcher.""" + conf = config[DOMAIN] + for watcher in conf: + path = watcher[CONF_FOLDER] + patterns = watcher[CONF_PATTERNS] + if not hass.config.is_allowed_path(path): + _LOGGER.error("folder %s is not valid or allowed", path) + return False + Watcher(path, patterns, hass) + + return True + + +def create_event_handler(patterns, hass): + """"Return the Watchdog EventHandler object.""" + from watchdog.events import PatternMatchingEventHandler + + class EventHandler(PatternMatchingEventHandler): + """Class for handling Watcher events.""" + + def __init__(self, patterns, hass): + """Initialise the EventHandler.""" + super().__init__(patterns) + self.hass = hass + + def process(self, event): + """On Watcher event, fire HA event.""" + _LOGGER.debug("process(%s)", event) + if not event.is_directory: + folder, file_name = os.path.split(event.src_path) + self.hass.bus.fire( + DOMAIN, { + "event_type": event.event_type, + 'path': event.src_path, + 'file': file_name, + 'folder': folder, + }) + + def on_modified(self, event): + """File modified.""" + self.process(event) + + def on_moved(self, event): + """File moved.""" + self.process(event) + + def on_created(self, event): + """File created.""" + self.process(event) + + def on_deleted(self, event): + """File deleted.""" + self.process(event) + + return EventHandler(patterns, hass) + + +class Watcher(): + """Class for starting Watchdog.""" + + def __init__(self, path, patterns, hass): + """Initialise the watchdog observer.""" + from watchdog.observers import Observer + self._observer = Observer() + self._observer.schedule( + create_event_handler(patterns, hass), + path, + recursive=True) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.startup) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown) + + def startup(self, event): + """Start the watcher.""" + self._observer.start() + + def shutdown(self, event): + """Shutdown the watcher.""" + self._observer.stop() + self._observer.join() diff --git a/requirements_all.txt b/requirements_all.txt index e79249151b1..d983028cead 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1279,6 +1279,9 @@ waqiasync==1.0.0 # homeassistant.components.cloud warrant==0.6.1 +# homeassistant.components.folder_watcher +watchdog==0.8.3 + # homeassistant.components.waterfurnace waterfurnace==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8a57488d80..6630c09c1c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -199,5 +199,8 @@ wakeonlan==1.0.0 # homeassistant.components.cloud warrant==0.6.1 +# homeassistant.components.folder_watcher +watchdog==0.8.3 + # homeassistant.components.sensor.yahoo_finance yahoo-finance==1.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d8fc7b1ed60..1f5348136c6 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -90,6 +90,7 @@ TEST_REQUIREMENTS = ( 'yahoo-finance', 'pythonwhois', 'wakeonlan', + 'watchdog', 'vultr' ) diff --git a/tests/components/test_folder_watcher.py b/tests/components/test_folder_watcher.py new file mode 100644 index 00000000000..587d8b7ad6d --- /dev/null +++ b/tests/components/test_folder_watcher.py @@ -0,0 +1,64 @@ +"""The tests for the folder_watcher component.""" +import unittest +from unittest.mock import MagicMock +import os + +from homeassistant.components import folder_watcher +from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant + +CWD = os.path.join(os.path.dirname(__file__)) +FILE = 'file.txt' + + +class TestFolderWatcher(unittest.TestCase): + """Test the file_watcher component.""" + + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.whitelist_external_dirs = set((CWD)) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_invalid_path_setup(self): + """Test that a invalid path is not setup.""" + config = { + folder_watcher.DOMAIN: [{ + folder_watcher.CONF_FOLDER: 'invalid_path' + }] + } + self.assertFalse( + setup_component(self.hass, folder_watcher.DOMAIN, config)) + + def test_valid_path_setup(self): + """Test that a valid path is setup.""" + config = { + folder_watcher.DOMAIN: [{folder_watcher.CONF_FOLDER: CWD}] + } + + self.assertTrue(setup_component( + self.hass, folder_watcher.DOMAIN, config)) + + def test_event(self): + """Check that HASS events are fired correctly on watchdog event.""" + from watchdog.events import FileModifiedEvent + + # Cant use setup_component as need to retrieve Watcher object. + w = folder_watcher.Watcher(CWD, + folder_watcher.DEFAULT_PATTERN, + self.hass) + w.startup(None) + + self.hass.bus.fire = MagicMock() + + # Trigger a fake filesystem event through the Watcher Observer emitter. + (emitter,) = w._observer.emitters + emitter.queue_event(FileModifiedEvent(FILE)) + + # Wait for the event to propagate. + self.hass.block_till_done() + + assert self.hass.bus.fire.called From 8617177ff136d8ef8bce69165dbd4010efafd331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85skar=20Andersson?= Date: Fri, 30 Mar 2018 04:45:25 +0200 Subject: [PATCH 054/136] Update rflink to 0.0.37 (#12603) * Update requirements_all.txt * Update rflink.py --- homeassistant/components/rflink.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index 439f938beb3..87e2a7a2331 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -22,7 +22,7 @@ from homeassistant.helpers.deprecation import get_deprecated from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['rflink==0.0.34'] +REQUIREMENTS = ['rflink==0.0.37'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d983028cead..b21452dc385 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1085,7 +1085,7 @@ regenmaschine==0.4.1 restrictedpython==4.0b2 # homeassistant.components.rflink -rflink==0.0.34 +rflink==0.0.37 # homeassistant.components.ring ring_doorbell==0.1.8 From 3e5462ebff2dfa8fe03e4c0c3a78f8fd2fad6049 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 30 Mar 2018 04:47:49 +0200 Subject: [PATCH 055/136] Added file path validity checks to file sensor (#12505) * Added file validity checks to file sensor * Patched out 'is_allowed_path' for file sensor tests --- homeassistant/components/sensor/file.py | 9 ++++++--- tests/components/sensor/test_file.py | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/file.py b/homeassistant/components/sensor/file.py index afa305a0fb0..cbdd4eef227 100644 --- a/homeassistant/components/sensor/file.py +++ b/homeassistant/components/sensor/file.py @@ -25,7 +25,7 @@ DEFAULT_NAME = 'File' ICON = 'mdi:file' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_FILE_PATH): cv.string, + vol.Required(CONF_FILE_PATH): cv.isfile, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, @@ -43,8 +43,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if value_template is not None: value_template.hass = hass - async_add_devices( - [FileSensor(name, file_path, unit, value_template)], True) + if hass.config.is_allowed_path(file_path): + async_add_devices( + [FileSensor(name, file_path, unit, value_template)], True) + else: + _LOGGER.error("'%s' is not a whitelisted directory", file_path) class FileSensor(Entity): diff --git a/tests/components/sensor/test_file.py b/tests/components/sensor/test_file.py index aa048f7a62e..7171289de69 100644 --- a/tests/components/sensor/test_file.py +++ b/tests/components/sensor/test_file.py @@ -18,6 +18,8 @@ class TestFileSensor(unittest.TestCase): def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + # Patch out 'is_allowed_path' as the mock files aren't allowed + self.hass.config.is_allowed_path = Mock(return_value=True) mock_registry(self.hass) def teardown_method(self, method): From 507c658fe9947be064c2e4188b22998efdec8fb1 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 30 Mar 2018 04:57:19 +0200 Subject: [PATCH 056/136] Check whitelisted paths #13107 (#13154) --- homeassistant/core.py | 10 +++++++--- tests/test_core.py | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 65db82a1fbe..feb8d331ae8 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1060,15 +1060,19 @@ class Config(object): """Check if the path is valid for access from outside.""" assert path is not None - parent = pathlib.Path(path) + thepath = pathlib.Path(path) try: - parent = parent.resolve() # pylint: disable=no-member + # The file path does not have to exist (it's parent should) + if thepath.exists(): + thepath = thepath.resolve() + else: + thepath = thepath.parent.resolve() except (FileNotFoundError, RuntimeError, PermissionError): return False for whitelisted_path in self.whitelist_external_dirs: try: - parent.relative_to(whitelisted_path) + thepath.relative_to(whitelisted_path) return True except ValueError: pass diff --git a/tests/test_core.py b/tests/test_core.py index 7a1610c0966..1fcd9416f36 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -809,7 +809,8 @@ class TestConfig(unittest.TestCase): valid = [ test_file, - tmp_dir + tmp_dir, + os.path.join(tmp_dir, 'notfound321') ] for path in valid: assert self.config.is_allowed_path(path) From 170763ef2ff4c8291417873ec9b9a0547ffe1fe6 Mon Sep 17 00:00:00 2001 From: Andy Castille Date: Thu, 29 Mar 2018 22:00:26 -0500 Subject: [PATCH 057/136] Allow for overriding the DoorBird push notification URL in configuration (#13268) * Allow for overriding the DoorBird push notification URL in configuration * rename override config key --- homeassistant/components/doorbird.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index 34758023f60..48f229b49ca 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -22,6 +22,7 @@ DOMAIN = 'doorbird' API_URL = '/api/{}'.format(DOMAIN) CONF_DOORBELL_EVENTS = 'doorbell_events' +CONF_CUSTOM_URL = 'hass_url_override' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -29,6 +30,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean, + vol.Optional(CONF_CUSTOM_URL): cv.string, }) }, extra=vol.ALLOW_EXTRA) @@ -61,9 +63,17 @@ def setup(hass, config): # Provide an endpoint for the device to call to trigger events hass.http.register_view(DoorbirdRequestView()) + # Get the URL of this server + hass_url = hass.config.api.base_url + + # Override it if another is specified in the component configuration + if config[DOMAIN].get(CONF_CUSTOM_URL): + hass_url = config[DOMAIN].get(CONF_CUSTOM_URL) + _LOGGER.info("DoorBird will connect to this instance via %s", + hass_url) + # This will make HA the only service that gets doorbell events - url = '{}{}/{}'.format( - hass.config.api.base_url, API_URL, SENSOR_DOORBELL) + url = '{}{}/{}'.format(hass_url, API_URL, SENSOR_DOORBELL) device.reset_notifications() device.subscribe_notification(SENSOR_DOORBELL, url) From 1ae8b6ee08adecc66a3081d471fee9b9d2d48435 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Mar 2018 20:02:21 -0700 Subject: [PATCH 058/136] Fix requirements --- requirements_test_all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6630c09c1c4..31a7874409a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -159,7 +159,7 @@ pywebpush==1.6.0 restrictedpython==4.0b2 # homeassistant.components.rflink -rflink==0.0.34 +rflink==0.0.37 # homeassistant.components.ring ring_doorbell==0.1.8 From 184f2be83e3c3f3824efa566fa29a779a57832e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Mar 2018 20:15:40 -0700 Subject: [PATCH 059/136] Convert Hue to always use config entries (#13034) --- homeassistant/components/discovery.py | 26 +- homeassistant/components/hue/__init__.py | 371 +++----------------- homeassistant/components/hue/bridge.py | 143 ++++++++ homeassistant/components/hue/config_flow.py | 235 +++++++++++++ homeassistant/components/hue/const.py | 6 + homeassistant/components/hue/errors.py | 14 + homeassistant/components/hue/strings.json | 5 +- homeassistant/config_entries.py | 2 +- tests/components/hue/conftest.py | 17 - tests/components/hue/test_bridge.py | 136 +++---- tests/components/hue/test_config_flow.py | 213 +++++++++-- tests/components/hue/test_init.py | 169 +++++++++ tests/components/hue/test_setup.py | 70 ---- tests/components/test_discovery.py | 34 +- 14 files changed, 914 insertions(+), 527 deletions(-) create mode 100644 homeassistant/components/hue/bridge.py create mode 100644 homeassistant/components/hue/config_flow.py create mode 100644 homeassistant/components/hue/const.py create mode 100644 homeassistant/components/hue/errors.py delete mode 100644 tests/components/hue/conftest.py create mode 100644 tests/components/hue/test_init.py delete mode 100644 tests/components/hue/test_setup.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index eb53782d698..b2aa5b890a8 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -13,6 +13,7 @@ import os import voluptuous as vol +from homeassistant import config_entries from homeassistant.core import callback from homeassistant.const import EVENT_HOMEASSISTANT_START import homeassistant.helpers.config_validation as cv @@ -40,6 +41,10 @@ SERVICE_DECONZ = 'deconz' SERVICE_DAIKIN = 'daikin' SERVICE_SAMSUNG_PRINTER = 'samsung_printer' +CONFIG_ENTRY_HANDLERS = { + SERVICE_HUE: 'hue', +} + SERVICE_HANDLERS = { SERVICE_HASS_IOS_APP: ('ios', None), SERVICE_NETGEAR: ('device_tracker', None), @@ -51,7 +56,6 @@ SERVICE_HANDLERS = { SERVICE_WINK: ('wink', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_TELLDUSLIVE: ('tellduslive', None), - SERVICE_HUE: ('hue', None), SERVICE_DECONZ: ('deconz', None), SERVICE_DAIKIN: ('daikin', None), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), @@ -105,6 +109,20 @@ async def async_setup(hass, config): logger.info("Ignoring service: %s %s", service, info) return + discovery_hash = json.dumps([service, info], sort_keys=True) + if discovery_hash in already_discovered: + return + + already_discovered.add(discovery_hash) + + if service in CONFIG_ENTRY_HANDLERS: + await hass.config_entries.flow.async_init( + CONFIG_ENTRY_HANDLERS[service], + source=config_entries.SOURCE_DISCOVERY, + data=info + ) + return + comp_plat = SERVICE_HANDLERS.get(service) # We do not know how to handle this service. @@ -112,12 +130,6 @@ async def async_setup(hass, config): logger.info("Unknown service discovered: %s %s", service, info) return - discovery_hash = json.dumps([service, info], sort_keys=True) - if discovery_hash in already_discovered: - return - - already_discovered.add(discovery_hash) - logger.info("Found new service: %s %s", service, info) component, platform = comp_plat diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index b70021e0304..557a47f3e05 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -4,31 +4,23 @@ This component provides basic support for the Philips Hue system. For more details about this component, please refer to the documentation at https://home-assistant.io/components/hue/ """ -import asyncio -import json import ipaddress import logging -import os -import async_timeout import voluptuous as vol -from homeassistant.core import callback -from homeassistant.components.discovery import SERVICE_HUE from homeassistant.const import CONF_FILENAME, CONF_HOST -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery, aiohttp_client -from homeassistant import config_entries -from homeassistant.util.json import save_json +from homeassistant.helpers import aiohttp_client, config_validation as cv + +from .const import DOMAIN, API_NUPNP +from .bridge import HueBridge +# Loading the config flow file will register the flow +from .config_flow import configured_hosts REQUIREMENTS = ['aiohue==1.3.0'] _LOGGER = logging.getLogger(__name__) -DOMAIN = "hue" -SERVICE_HUE_SCENE = "hue_activate_scene" -API_NUPNP = 'https://www.meethue.com/api/nupnp' - CONF_BRIDGES = "bridges" CONF_ALLOW_UNREACHABLE = 'allow_unreachable' @@ -42,6 +34,7 @@ DEFAULT_ALLOW_HUE_GROUPS = True BRIDGE_CONFIG_SCHEMA = vol.Schema({ # Validate as IP address and then convert back to a string. vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), + # This is for legacy reasons and is only used for importing auth. vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string, vol.Optional(CONF_ALLOW_UNREACHABLE, default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean, @@ -56,19 +49,6 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) -ATTR_GROUP_NAME = "group_name" -ATTR_SCENE_NAME = "scene_name" -SCENE_SCHEMA = vol.Schema({ - vol.Required(ATTR_GROUP_NAME): cv.string, - vol.Required(ATTR_SCENE_NAME): cv.string, -}) - -CONFIG_INSTRUCTIONS = """ -Press the button on the bridge to register Philips Hue with Home Assistant. - -![Location of button on bridge](/static/images/config_philips_hue.jpg) -""" - async def async_setup(hass, config): """Set up the Hue platform.""" @@ -76,20 +56,8 @@ async def async_setup(hass, config): if conf is None: conf = {} - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} - - async def async_bridge_discovered(service, discovery_info): - """Dispatcher for Hue discovery events.""" - # Ignore emulated hue - if "HASS Bridge" in discovery_info.get('name', ''): - return - - await async_setup_bridge( - hass, discovery_info['host'], - 'phue-{}.conf'.format(discovery_info['serial'])) - - discovery.async_listen(hass, SERVICE_HUE, async_bridge_discovered) + hass.data[DOMAIN] = {} + configured = configured_hosts(hass) # User has configured bridges if CONF_BRIDGES in conf: @@ -103,12 +71,19 @@ async def async_setup(hass, config): async with websession.get(API_NUPNP) as req: hosts = await req.json() - # Run through config schema to populate defaults - bridges = [BRIDGE_CONFIG_SCHEMA({ - CONF_HOST: entry['internalipaddress'], - CONF_FILENAME: '.hue_{}.conf'.format(entry['id']), - }) for entry in hosts] + bridges = [] + for entry in hosts: + # Filter out already configured hosts + if entry['internalipaddress'] in configured: + continue + # Run through config schema to populate defaults + bridges.append(BRIDGE_CONFIG_SCHEMA({ + CONF_HOST: entry['internalipaddress'], + # Careful with using entry['id'] for other reasons. The + # value is in lowercase but is returned uppercase from hub. + CONF_FILENAME: '.hue_{}.conf'.format(entry['id']), + })) else: # Component not specified in config, we're loaded via discovery bridges = [] @@ -116,277 +91,43 @@ async def async_setup(hass, config): if not bridges: return True - await asyncio.wait([ - async_setup_bridge( - hass, bridge[CONF_HOST], bridge[CONF_FILENAME], - bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS] - ) for bridge in bridges - ]) + for bridge_conf in bridges: + host = bridge_conf[CONF_HOST] + + # Store config in hass.data so the config entry can find it + hass.data[DOMAIN][host] = bridge_conf + + # If configured, the bridge will be set up during config entry phase + if host in configured: + continue + + # No existing config entry found, try importing it or trigger link + # config flow if no existing auth. Because we're inside the setup of + # this component we'll have to use hass.async_add_job to avoid a + # deadlock: creating a config entry will set up the component but the + # setup would block till the entry is created! + hass.async_add_job(hass.config_entries.flow.async_init( + DOMAIN, source='import', data={ + 'host': bridge_conf[CONF_HOST], + 'path': bridge_conf[CONF_FILENAME], + } + )) return True -async def async_setup_bridge( - hass, host, filename=None, - allow_unreachable=DEFAULT_ALLOW_UNREACHABLE, - allow_hue_groups=DEFAULT_ALLOW_HUE_GROUPS, - username=None): - """Set up a given Hue bridge.""" - assert filename or username, 'Need to pass at least a username or filename' - - # Only register a device once - if host in hass.data[DOMAIN]: - return - - if username is None: - username = await hass.async_add_job( - _find_username_from_config, hass, filename) - - bridge = HueBridge(host, hass, filename, username, allow_unreachable, - allow_hue_groups) - await bridge.async_setup() - - -def _find_username_from_config(hass, filename): - """Load username from config.""" - path = hass.config.path(filename) - - if not os.path.isfile(path): - return None - - with open(path) as inp: - return list(json.load(inp).values())[0]['username'] - - -class HueBridge(object): - """Manages a single Hue bridge.""" - - def __init__(self, host, hass, filename, username, - allow_unreachable=False, allow_groups=True): - """Initialize the system.""" - self.host = host - self.hass = hass - self.filename = filename - self.username = username - self.allow_unreachable = allow_unreachable - self.allow_groups = allow_groups - self.available = True - self.config_request_id = None - self.api = None - - async def async_setup(self): - """Set up a phue bridge based on host parameter.""" - import aiohue - - api = aiohue.Bridge( - self.host, - username=self.username, - websession=aiohttp_client.async_get_clientsession(self.hass) - ) - - try: - with async_timeout.timeout(5): - # Initialize bridge and validate our username - if not self.username: - await api.create_user('home-assistant') - await api.initialize() - except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): - _LOGGER.warning("Connected to Hue at %s but not registered.", - self.host) - self.async_request_configuration() - return - except (asyncio.TimeoutError, aiohue.RequestError): - _LOGGER.error("Error connecting to the Hue bridge at %s", - self.host) - return - except aiohue.AiohueException: - _LOGGER.exception('Unknown Hue linking error occurred') - self.async_request_configuration() - return - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unknown error connecting with Hue bridge at %s", - self.host) - return - - self.hass.data[DOMAIN][self.host] = self - - # If we came here and configuring this host, mark as done - if self.config_request_id: - request_id = self.config_request_id - self.config_request_id = None - self.hass.components.configurator.async_request_done(request_id) - - self.username = api.username - - # Save config file - await self.hass.async_add_job( - save_json, self.hass.config.path(self.filename), - {self.host: {'username': api.username}}) - - self.api = api - - self.hass.async_add_job(discovery.async_load_platform( - self.hass, 'light', DOMAIN, - {'host': self.host})) - - self.hass.services.async_register( - DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, - schema=SCENE_SCHEMA) - - @callback - def async_request_configuration(self): - """Request configuration steps from the user.""" - configurator = self.hass.components.configurator - - # We got an error if this method is called while we are configuring - if self.config_request_id: - configurator.async_notify_errors( - self.config_request_id, - "Failed to register, please try again.") - return - - async def config_callback(data): - """Callback for configurator data.""" - await self.async_setup() - - self.config_request_id = configurator.async_request_config( - "Philips Hue", config_callback, - description=CONFIG_INSTRUCTIONS, - entity_picture="/static/images/logo_philips_hue.png", - submit_caption="I have pressed the button" - ) - - async def hue_activate_scene(self, call, updated=False): - """Service to call directly into bridge to set scenes.""" - group_name = call.data[ATTR_GROUP_NAME] - scene_name = call.data[ATTR_SCENE_NAME] - - group = next( - (group for group in self.api.groups.values() - if group.name == group_name), None) - - scene_id = next( - (scene.id for scene in self.api.scenes.values() - if scene.name == scene_name), None) - - # If we can't find it, fetch latest info. - if not updated and (group is None or scene_id is None): - await self.api.groups.update() - await self.api.scenes.update() - await self.hue_activate_scene(call, updated=True) - return - - if group is None: - _LOGGER.warning('Unable to find group %s', group_name) - return - - if scene_id is None: - _LOGGER.warning('Unable to find scene %s', scene_name) - return - - await group.set_action(scene=scene_id) - - -@config_entries.HANDLERS.register(DOMAIN) -class HueFlowHandler(config_entries.ConfigFlowHandler): - """Handle a Hue config flow.""" - - VERSION = 1 - - def __init__(self): - """Initialize the Hue flow.""" - self.host = None - - @property - def _websession(self): - """Return a websession. - - Cannot assign in init because hass variable is not set yet. - """ - return aiohttp_client.async_get_clientsession(self.hass) - - async def async_step_init(self, user_input=None): - """Handle a flow start.""" - from aiohue.discovery import discover_nupnp - - if user_input is not None: - self.host = user_input['host'] - return await self.async_step_link() - - try: - with async_timeout.timeout(5): - bridges = await discover_nupnp(websession=self._websession) - except asyncio.TimeoutError: - return self.async_abort( - reason='discover_timeout' - ) - - if not bridges: - return self.async_abort( - reason='no_bridges' - ) - - # Find already configured hosts - configured_hosts = set( - entry.data['host'] for entry - in self.hass.config_entries.async_entries(DOMAIN)) - - hosts = [bridge.host for bridge in bridges - if bridge.host not in configured_hosts] - - if not hosts: - return self.async_abort( - reason='all_configured' - ) - - elif len(hosts) == 1: - self.host = hosts[0] - return await self.async_step_link() - - return self.async_show_form( - step_id='init', - data_schema=vol.Schema({ - vol.Required('host'): vol.In(hosts) - }) - ) - - async def async_step_link(self, user_input=None): - """Attempt to link with the Hue bridge.""" - import aiohue - errors = {} - - if user_input is not None: - bridge = aiohue.Bridge(self.host, websession=self._websession) - try: - with async_timeout.timeout(5): - # Create auth token - await bridge.create_user('home-assistant') - # Fetches name and id - await bridge.initialize() - except (asyncio.TimeoutError, aiohue.RequestError, - aiohue.LinkButtonNotPressed): - errors['base'] = 'register_failed' - except aiohue.AiohueException: - errors['base'] = 'linking' - _LOGGER.exception('Unknown Hue linking error occurred') - else: - return self.async_create_entry( - title=bridge.config.name, - data={ - 'host': bridge.host, - 'bridge_id': bridge.config.bridgeid, - 'username': bridge.username, - } - ) - - return self.async_show_form( - step_id='link', - errors=errors, - ) - - async def async_setup_entry(hass, entry): - """Set up a bridge for a config entry.""" - await async_setup_bridge(hass, entry.data['host'], - username=entry.data['username']) - return True + """Set up a bridge from a config entry.""" + host = entry.data['host'] + config = hass.data[DOMAIN].get(host) + + if config is None: + allow_unreachable = DEFAULT_ALLOW_UNREACHABLE + allow_groups = DEFAULT_ALLOW_HUE_GROUPS + else: + allow_unreachable = config[CONF_ALLOW_UNREACHABLE] + allow_groups = config[CONF_ALLOW_HUE_GROUPS] + + bridge = HueBridge(hass, entry, allow_unreachable, allow_groups) + hass.data[DOMAIN][host] = bridge + return await bridge.async_setup() diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py new file mode 100644 index 00000000000..790831a4d6c --- /dev/null +++ b/homeassistant/components/hue/bridge.py @@ -0,0 +1,143 @@ +"""Code to handle a Hue bridge.""" +import asyncio + +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.helpers import aiohttp_client, config_validation as cv + +from .const import DOMAIN, LOGGER +from .errors import AuthenticationRequired, CannotConnect + +SERVICE_HUE_SCENE = "hue_activate_scene" +ATTR_GROUP_NAME = "group_name" +ATTR_SCENE_NAME = "scene_name" +SCENE_SCHEMA = vol.Schema({ + vol.Required(ATTR_GROUP_NAME): cv.string, + vol.Required(ATTR_SCENE_NAME): cv.string, +}) + + +class HueBridge(object): + """Manages a single Hue bridge.""" + + def __init__(self, hass, config_entry, allow_unreachable, allow_groups): + """Initialize the system.""" + self.config_entry = config_entry + self.hass = hass + self.allow_unreachable = allow_unreachable + self.allow_groups = allow_groups + self.available = True + self.api = None + + async def async_setup(self, tries=0): + """Set up a phue bridge based on host parameter.""" + host = self.config_entry.data['host'] + + try: + self.api = await get_bridge( + self.hass, host, + self.config_entry.data['username'] + ) + except AuthenticationRequired: + # usernames can become invalid if hub is reset or user removed. + # We are going to fail the config entry setup and initiate a new + # linking procedure. When linking succeeds, it will remove the + # old config entry. + self.hass.async_add_job(self.hass.config_entries.flow.async_init( + DOMAIN, source='import', data={ + 'host': host, + } + )) + return False + + except CannotConnect: + retry_delay = 2 ** (tries + 1) + LOGGER.error("Error connecting to the Hue bridge at %s. Retrying " + "in %d seconds", host, retry_delay) + + async def retry_setup(_now): + """Retry setup.""" + if await self.async_setup(tries + 1): + # This feels hacky, we should find a better way to do this + self.config_entry.state = config_entries.ENTRY_STATE_LOADED + + # Unhandled edge case: cancel this if we discover bridge on new IP + self.hass.helpers.event.async_call_later(retry_delay, retry_setup) + + return False + + except Exception: # pylint: disable=broad-except + LOGGER.exception('Unknown error connecting with Hue bridge at %s', + host) + return False + + self.hass.async_add_job( + self.hass.helpers.discovery.async_load_platform( + 'light', DOMAIN, {'host': host})) + + self.hass.services.async_register( + DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, + schema=SCENE_SCHEMA) + + return True + + async def hue_activate_scene(self, call, updated=False): + """Service to call directly into bridge to set scenes.""" + group_name = call.data[ATTR_GROUP_NAME] + scene_name = call.data[ATTR_SCENE_NAME] + + group = next( + (group for group in self.api.groups.values() + if group.name == group_name), None) + + scene_id = next( + (scene.id for scene in self.api.scenes.values() + if scene.name == scene_name), None) + + # If we can't find it, fetch latest info. + if not updated and (group is None or scene_id is None): + await self.api.groups.update() + await self.api.scenes.update() + await self.hue_activate_scene(call, updated=True) + return + + if group is None: + LOGGER.warning('Unable to find group %s', group_name) + return + + if scene_id is None: + LOGGER.warning('Unable to find scene %s', scene_name) + return + + await group.set_action(scene=scene_id) + + +async def get_bridge(hass, host, username=None): + """Create a bridge object and verify authentication.""" + import aiohue + + bridge = aiohue.Bridge( + host, username=username, + websession=aiohttp_client.async_get_clientsession(hass) + ) + + try: + with async_timeout.timeout(5): + # Create username if we don't have one + if not username: + await bridge.create_user('home-assistant') + # Initialize bridge (and validate our username) + await bridge.initialize() + + return bridge + except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): + LOGGER.warning("Connected to Hue at %s but not registered.", host) + raise AuthenticationRequired + except (asyncio.TimeoutError, aiohue.RequestError): + LOGGER.error("Error connecting to the Hue bridge at %s", host) + raise CannotConnect + except aiohue.AiohueException: + LOGGER.exception('Unknown Hue linking error occurred') + raise AuthenticationRequired diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py new file mode 100644 index 00000000000..11e399c984d --- /dev/null +++ b/homeassistant/components/hue/config_flow.py @@ -0,0 +1,235 @@ +"""Config flow to configure Philips Hue.""" +import asyncio +import json +import os + +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client + +from .bridge import get_bridge +from .const import DOMAIN, LOGGER +from .errors import AuthenticationRequired, CannotConnect + + +@callback +def configured_hosts(hass): + """Return a set of the configured hosts.""" + return set(entry.data['host'] for entry + in hass.config_entries.async_entries(DOMAIN)) + + +def _find_username_from_config(hass, filename): + """Load username from config. + + This was a legacy way of configuring Hue until Home Assistant 0.67. + """ + path = hass.config.path(filename) + + if not os.path.isfile(path): + return None + + with open(path) as inp: + try: + return list(json.load(inp).values())[0]['username'] + except ValueError: + # If we get invalid JSON + return None + + +@config_entries.HANDLERS.register(DOMAIN) +class HueFlowHandler(config_entries.ConfigFlowHandler): + """Handle a Hue config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize the Hue flow.""" + self.host = None + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + from aiohue.discovery import discover_nupnp + + if user_input is not None: + self.host = user_input['host'] + return await self.async_step_link() + + websession = aiohttp_client.async_get_clientsession(self.hass) + + try: + with async_timeout.timeout(5): + bridges = await discover_nupnp(websession=websession) + except asyncio.TimeoutError: + return self.async_abort( + reason='discover_timeout' + ) + + if not bridges: + return self.async_abort( + reason='no_bridges' + ) + + # Find already configured hosts + configured = configured_hosts(self.hass) + + hosts = [bridge.host for bridge in bridges + if bridge.host not in configured] + + if not hosts: + return self.async_abort( + reason='all_configured' + ) + + elif len(hosts) == 1: + self.host = hosts[0] + return await self.async_step_link() + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema({ + vol.Required('host'): vol.In(hosts) + }) + ) + + async def async_step_link(self, user_input=None): + """Attempt to link with the Hue bridge. + + Given a configured host, will ask the user to press the link button + to connect to the bridge. + """ + errors = {} + + # We will always try linking in case the user has already pressed + # the link button. + try: + bridge = await get_bridge( + self.hass, self.host, username=None + ) + + return await self._entry_from_bridge(bridge) + except AuthenticationRequired: + errors['base'] = 'register_failed' + + except CannotConnect: + LOGGER.error("Error connecting to the Hue bridge at %s", self.host) + errors['base'] = 'linking' + + except Exception: # pylint: disable=broad-except + LOGGER.exception( + 'Unknown error connecting with Hue bridge at %s', + self.host) + errors['base'] = 'linking' + + # If there was no user input, do not show the errors. + if user_input is None: + errors = {} + + return self.async_show_form( + step_id='link', + errors=errors, + ) + + async def async_step_discovery(self, discovery_info): + """Handle a discovered Hue bridge. + + This flow is triggered by the discovery component. It will check if the + host is already configured and delegate to the import step if not. + """ + # Filter out emulated Hue + if "HASS Bridge" in discovery_info.get('name', ''): + return self.async_abort(reason='already_configured') + + host = discovery_info.get('host') + + if host in configured_hosts(self.hass): + return self.async_abort(reason='already_configured') + + # This value is based off host/description.xml and is, weirdly, missing + # 4 characters in the middle of the serial compared to results returned + # from the NUPNP API or when querying the bridge API for bridgeid. + # (on first gen Hue hub) + serial = discovery_info.get('serial') + + return await self.async_step_import({ + 'host': host, + # This format is the legacy format that Hue used for discovery + 'path': 'phue-{}.conf'.format(serial) + }) + + async def async_step_import(self, import_info): + """Import a new bridge as a config entry. + + Will read authentication from Phue config file if available. + + This flow is triggered by `async_setup` for both configured and + discovered bridges. Triggered for any bridge that does not have a + config entry yet (based on host). + + This flow is also triggered by `async_step_discovery`. + + If an existing config file is found, we will validate the credentials + and create an entry. Otherwise we will delegate to `link` step which + will ask user to link the bridge. + """ + host = import_info['host'] + path = import_info.get('path') + + if path is not None: + username = await self.hass.async_add_job( + _find_username_from_config, self.hass, + self.hass.config.path(path)) + else: + username = None + + try: + bridge = await get_bridge( + self.hass, host, username + ) + + LOGGER.info('Imported authentication for %s from %s', host, path) + + return await self._entry_from_bridge(bridge) + except AuthenticationRequired: + self.host = host + + LOGGER.info('Invalid authentication for %s, requesting link.', + host) + + return await self.async_step_link() + + except CannotConnect: + LOGGER.error("Error connecting to the Hue bridge at %s", host) + return self.async_abort(reason='cannot_connect') + + except Exception: # pylint: disable=broad-except + LOGGER.exception('Unknown error connecting with Hue bridge at %s', + host) + return self.async_abort(reason='unknown') + + async def _entry_from_bridge(self, bridge): + """Return a config entry from an initialized bridge.""" + # Remove all other entries of hubs with same ID or host + host = bridge.host + bridge_id = bridge.config.bridgeid + + same_hub_entries = [entry.entry_id for entry + in self.hass.config_entries.async_entries(DOMAIN) + if entry.data['bridge_id'] == bridge_id or + entry.data['host'] == host] + + if same_hub_entries: + await asyncio.wait([self.hass.config_entries.async_remove(entry_id) + for entry_id in same_hub_entries]) + + return self.async_create_entry( + title=bridge.config.name, + data={ + 'host': host, + 'bridge_id': bridge_id, + 'username': bridge.username, + } + ) diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py new file mode 100644 index 00000000000..2eb30d47804 --- /dev/null +++ b/homeassistant/components/hue/const.py @@ -0,0 +1,6 @@ +"""Constants for the Hue component.""" +import logging + +LOGGER = logging.getLogger('homeassistant.components.hue') +DOMAIN = "hue" +API_NUPNP = 'https://www.meethue.com/api/nupnp' diff --git a/homeassistant/components/hue/errors.py b/homeassistant/components/hue/errors.py new file mode 100644 index 00000000000..dd217c3bc26 --- /dev/null +++ b/homeassistant/components/hue/errors.py @@ -0,0 +1,14 @@ +"""Errors for the Hue component.""" +from homeassistant.exceptions import HomeAssistantError + + +class HueException(HomeAssistantError): + """Base class for Hue exceptions.""" + + +class CannotConnect(HueException): + """Unable to connect to the bridge.""" + + +class AuthenticationRequired(HueException): + """Unknown error occurred.""" diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 59b1ecd3cd1..fc9e91c93d7 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -20,7 +20,10 @@ "abort": { "discover_timeout": "Unable to discover Hue bridges", "no_bridges": "No Philips Hue bridges discovered", - "all_configured": "All Philips Hue bridges are already configured" + "all_configured": "All Philips Hue bridges are already configured", + "unknown": "Unknown error occurred", + "cannot_connect": "Unable to connect to the bridge", + "already_configured": "Bridge is already configured" } } } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index eb05e800683..b02026ac6dd 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -384,7 +384,7 @@ class FlowManager: handler = HANDLERS.get(domain) if handler is None: - raise self.hass.helpers.UnknownHandler + raise UnknownHandler # Make sure requirements and dependencies of component are resolved await async_process_deps_reqs( diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py deleted file mode 100644 index 7ccc202b31b..00000000000 --- a/tests/components/hue/conftest.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Fixtures for Hue tests.""" -from unittest.mock import patch - -import pytest - -from tests.common import mock_coro_func - - -@pytest.fixture -def mock_bridge(): - """Mock the HueBridge from initializing.""" - with patch('homeassistant.components.hue._find_username_from_config', - return_value=None), \ - patch('homeassistant.components.hue.HueBridge') as mock_bridge: - mock_bridge().async_setup = mock_coro_func() - mock_bridge.reset_mock() - yield mock_bridge diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 39351699df5..0845aa2f077 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -1,99 +1,57 @@ """Test Hue bridge.""" -import asyncio from unittest.mock import Mock, patch -import aiohue -import pytest - -from homeassistant.components import hue +from homeassistant.components.hue import bridge, errors from tests.common import mock_coro -class MockBridge(hue.HueBridge): - """Class that sets default for constructor.""" +async def test_bridge_setup(): + """Test a successful setup.""" + hass = Mock() + entry = Mock() + api = Mock() + entry.data = {'host': '1.2.3.4', 'username': 'mock-username'} + hue_bridge = bridge.HueBridge(hass, entry, False, False) - def __init__(self, hass, host='1.2.3.4', filename='mock-bridge.conf', - username=None, **kwargs): - """Initialize a mock bridge.""" - super().__init__(host, hass, filename, username, **kwargs) + with patch.object(bridge, 'get_bridge', return_value=mock_coro(api)): + assert await hue_bridge.async_setup() is True - -@pytest.fixture -def mock_request(): - """Mock configurator.async_request_config.""" - with patch('homeassistant.components.configurator.' - 'async_request_config') as mock_request: - yield mock_request - - -async def test_setup_request_config_button_not_pressed(hass, mock_request): - """Test we request config if link button has not been pressed.""" - with patch('aiohue.Bridge.create_user', - side_effect=aiohue.LinkButtonNotPressed): - await MockBridge(hass).async_setup() - - assert len(mock_request.mock_calls) == 1 - - -async def test_setup_request_config_invalid_username(hass, mock_request): - """Test we request config if username is no longer whitelisted.""" - with patch('aiohue.Bridge.create_user', - side_effect=aiohue.Unauthorized): - await MockBridge(hass).async_setup() - - assert len(mock_request.mock_calls) == 1 - - -async def test_setup_timeout(hass, mock_request): - """Test we give up when there is a timeout.""" - with patch('aiohue.Bridge.create_user', - side_effect=asyncio.TimeoutError): - await MockBridge(hass).async_setup() - - assert len(mock_request.mock_calls) == 0 - - -async def test_only_create_no_username(hass): - """.""" - with patch('aiohue.Bridge.create_user') as mock_create, \ - patch('aiohue.Bridge.initialize') as mock_init: - await MockBridge(hass, username='bla').async_setup() - - assert len(mock_create.mock_calls) == 0 - assert len(mock_init.mock_calls) == 1 - - -async def test_configurator_callback(hass, mock_request): - """.""" - hass.data[hue.DOMAIN] = {} - with patch('aiohue.Bridge.create_user', - side_effect=aiohue.LinkButtonNotPressed): - await MockBridge(hass).async_setup() - - assert len(mock_request.mock_calls) == 1 - - callback = mock_request.mock_calls[0][1][2] - - mock_init = Mock(return_value=mock_coro()) - mock_create = Mock(return_value=mock_coro()) - - with patch('aiohue.Bridge') as mock_bridge, \ - patch('homeassistant.helpers.discovery.async_load_platform', - return_value=mock_coro()) as mock_load_platform, \ - patch('homeassistant.components.hue.save_json') as mock_save: - inst = mock_bridge() - inst.username = 'mock-user' - inst.create_user = mock_create - inst.initialize = mock_init - await callback(None) - - assert len(mock_create.mock_calls) == 1 - assert len(mock_init.mock_calls) == 1 - assert len(mock_save.mock_calls) == 1 - assert mock_save.mock_calls[0][1][1] == { - '1.2.3.4': { - 'username': 'mock-user' - } + assert hue_bridge.api is api + assert len(hass.helpers.discovery.async_load_platform.mock_calls) == 1 + assert hass.helpers.discovery.async_load_platform.mock_calls[0][1][2] == { + 'host': '1.2.3.4' } - assert len(mock_load_platform.mock_calls) == 1 + + +async def test_bridge_setup_invalid_username(): + """Test we start config flow if username is no longer whitelisted.""" + hass = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'username': 'mock-username'} + hue_bridge = bridge.HueBridge(hass, entry, False, False) + + with patch.object(bridge, 'get_bridge', + side_effect=errors.AuthenticationRequired): + assert await hue_bridge.async_setup() is False + + assert len(hass.async_add_job.mock_calls) == 1 + assert len(hass.config_entries.flow.async_init.mock_calls) == 1 + assert hass.config_entries.flow.async_init.mock_calls[0][2]['data'] == { + 'host': '1.2.3.4' + } + + +async def test_bridge_setup_timeout(hass): + """Test we retry to connect if we cannot connect.""" + hass = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'username': 'mock-username'} + hue_bridge = bridge.HueBridge(hass, entry, False, False) + + with patch.object(bridge, 'get_bridge', side_effect=errors.CannotConnect): + assert await hue_bridge.async_setup() is False + + assert len(hass.helpers.event.async_call_later.mock_calls) == 1 + # Assert we are going to wait 2 seconds + assert hass.helpers.event.async_call_later.mock_calls[0][1][0] == 2 diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 959e3c6241b..fe3bffe5357 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -1,28 +1,29 @@ """Tests for Philips Hue config flow.""" import asyncio -from unittest.mock import patch +from unittest.mock import Mock, patch import aiohue import pytest import voluptuous as vol -from homeassistant.components import hue +from homeassistant.components.hue import config_flow, const, errors from tests.common import MockConfigEntry, mock_coro async def test_flow_works(hass, aioclient_mock): """Test config flow .""" - aioclient_mock.get(hue.API_NUPNP, json=[ + aioclient_mock.get(const.API_NUPNP, json=[ {'internalipaddress': '1.2.3.4', 'id': 'bla'} ]) - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass await flow.async_step_init() with patch('aiohue.Bridge') as mock_bridge: - def mock_constructor(host, websession): + def mock_constructor(host, websession, username=None): + """Fake the bridge constructor.""" mock_bridge.host = host return mock_bridge @@ -50,8 +51,8 @@ async def test_flow_works(hass, aioclient_mock): async def test_flow_no_discovered_bridges(hass, aioclient_mock): """Test config flow discovers no bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[]) - flow = hue.HueFlowHandler() + aioclient_mock.get(const.API_NUPNP, json=[]) + flow = config_flow.HueFlowHandler() flow.hass = hass result = await flow.async_step_init() @@ -60,13 +61,13 @@ async def test_flow_no_discovered_bridges(hass, aioclient_mock): async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): """Test config flow discovers only already configured bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ + aioclient_mock.get(const.API_NUPNP, json=[ {'internalipaddress': '1.2.3.4', 'id': 'bla'} ]) MockConfigEntry(domain='hue', data={ 'host': '1.2.3.4' }).add_to_hass(hass) - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass result = await flow.async_step_init() @@ -75,10 +76,10 @@ async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): async def test_flow_one_bridge_discovered(hass, aioclient_mock): """Test config flow discovers one bridge.""" - aioclient_mock.get(hue.API_NUPNP, json=[ + aioclient_mock.get(const.API_NUPNP, json=[ {'internalipaddress': '1.2.3.4', 'id': 'bla'} ]) - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass result = await flow.async_step_init() @@ -88,11 +89,11 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock): async def test_flow_two_bridges_discovered(hass, aioclient_mock): """Test config flow discovers two bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ + aioclient_mock.get(const.API_NUPNP, json=[ {'internalipaddress': '1.2.3.4', 'id': 'bla'}, {'internalipaddress': '5.6.7.8', 'id': 'beer'} ]) - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass result = await flow.async_step_init() @@ -108,14 +109,14 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock): async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): """Test config flow discovers two bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ + aioclient_mock.get(const.API_NUPNP, json=[ {'internalipaddress': '1.2.3.4', 'id': 'bla'}, {'internalipaddress': '5.6.7.8', 'id': 'beer'} ]) MockConfigEntry(domain='hue', data={ 'host': '1.2.3.4' }).add_to_hass(hass) - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass result = await flow.async_step_init() @@ -126,7 +127,7 @@ async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): async def test_flow_timeout_discovery(hass): """Test config flow .""" - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass with patch('aiohue.discovery.discover_nupnp', @@ -138,7 +139,7 @@ async def test_flow_timeout_discovery(hass): async def test_flow_link_timeout(hass): """Test config flow .""" - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass with patch('aiohue.Bridge.create_user', @@ -148,13 +149,13 @@ async def test_flow_link_timeout(hass): assert result['type'] == 'form' assert result['step_id'] == 'link' assert result['errors'] == { - 'base': 'register_failed' + 'base': 'linking' } async def test_flow_link_button_not_pressed(hass): """Test config flow .""" - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass with patch('aiohue.Bridge.create_user', @@ -170,7 +171,7 @@ async def test_flow_link_button_not_pressed(hass): async def test_flow_link_unknown_host(hass): """Test config flow .""" - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass with patch('aiohue.Bridge.create_user', @@ -180,5 +181,175 @@ async def test_flow_link_unknown_host(hass): assert result['type'] == 'form' assert result['step_id'] == 'link' assert result['errors'] == { - 'base': 'register_failed' + 'base': 'linking' } + + +async def test_bridge_discovery(hass): + """Test a bridge being discovered.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + + with patch.object(config_flow, 'get_bridge', + side_effect=errors.AuthenticationRequired): + result = await flow.async_step_discovery({ + 'host': '0.0.0.0', + 'serial': '1234' + }) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_bridge_discovery_emulated_hue(hass): + """Test if discovery info is from an emulated hue instance.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_discovery({ + 'name': 'HASS Bridge', + 'host': '0.0.0.0', + 'serial': '1234' + }) + + assert result['type'] == 'abort' + + +async def test_bridge_discovery_already_configured(hass): + """Test if a discovered bridge has already been configured.""" + MockConfigEntry(domain='hue', data={ + 'host': '0.0.0.0' + }).add_to_hass(hass) + + flow = config_flow.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_discovery({ + 'host': '0.0.0.0', + 'serial': '1234' + }) + + assert result['type'] == 'abort' + + +async def test_import_with_existing_config(hass): + """Test importing a host with an existing config file.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + + bridge = Mock() + bridge.username = 'username-abc' + bridge.config.bridgeid = 'bridge-id-1234' + bridge.config.name = 'Mock Bridge' + bridge.host = '0.0.0.0' + + with patch.object(config_flow, '_find_username_from_config', + return_value='mock-user'), \ + patch.object(config_flow, 'get_bridge', + return_value=mock_coro(bridge)): + result = await flow.async_step_import({ + 'host': '0.0.0.0', + 'path': 'bla.conf' + }) + + assert result['type'] == 'create_entry' + assert result['title'] == 'Mock Bridge' + assert result['data'] == { + 'host': '0.0.0.0', + 'bridge_id': 'bridge-id-1234', + 'username': 'username-abc' + } + + +async def test_import_with_no_config(hass): + """Test importing a host without an existing config file.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + + with patch.object(config_flow, 'get_bridge', + side_effect=errors.AuthenticationRequired): + result = await flow.async_step_import({ + 'host': '0.0.0.0', + }) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_import_with_existing_but_invalid_config(hass): + """Test importing a host with a config file with invalid username.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + + with patch.object(config_flow, '_find_username_from_config', + return_value='mock-user'), \ + patch.object(config_flow, 'get_bridge', + side_effect=errors.AuthenticationRequired): + result = await flow.async_step_import({ + 'host': '0.0.0.0', + 'path': 'bla.conf' + }) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_import_cannot_connect(hass): + """Test importing a host that we cannot conncet to.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + + with patch.object(config_flow, 'get_bridge', + side_effect=errors.CannotConnect): + result = await flow.async_step_import({ + 'host': '0.0.0.0', + }) + + assert result['type'] == 'abort' + assert result['reason'] == 'cannot_connect' + + +async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): + """Test that we clean up entries for same host and bridge. + + An IP can only hold a single bridge and a single bridge can only be + accessible via a single IP. So when we create a new entry, we'll remove + all existing entries that either have same IP or same bridge_id. + """ + MockConfigEntry(domain='hue', data={ + 'host': '0.0.0.0', + 'bridge_id': 'id-1234' + }).add_to_hass(hass) + + MockConfigEntry(domain='hue', data={ + 'host': '1.2.3.4', + 'bridge_id': 'id-1234' + }).add_to_hass(hass) + + assert len(hass.config_entries.async_entries('hue')) == 2 + + flow = config_flow.HueFlowHandler() + flow.hass = hass + + bridge = Mock() + bridge.username = 'username-abc' + bridge.config.bridgeid = 'id-1234' + bridge.config.name = 'Mock Bridge' + bridge.host = '0.0.0.0' + + with patch.object(config_flow, 'get_bridge', + return_value=mock_coro(bridge)): + result = await flow.async_step_import({ + 'host': '0.0.0.0', + }) + + assert result['type'] == 'create_entry' + assert result['title'] == 'Mock Bridge' + assert result['data'] == { + 'host': '0.0.0.0', + 'bridge_id': 'id-1234', + 'username': 'username-abc' + } + # We did not process the result of this entry but already removed the old + # ones. So we should have 0 entries. + assert len(hass.config_entries.async_entries('hue')) == 0 diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py new file mode 100644 index 00000000000..47e74b70e83 --- /dev/null +++ b/tests/components/hue/test_init.py @@ -0,0 +1,169 @@ +"""Test Hue setup process.""" +from unittest.mock import patch + +from homeassistant.setup import async_setup_component +from homeassistant.components import hue + +from tests.common import mock_coro, MockConfigEntry + + +async def test_setup_with_no_config(hass): + """Test that we do not discover anything or try to setup a bridge.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hue, 'configured_hosts', return_value=[]): + assert await async_setup_component(hass, hue.DOMAIN, {}) is True + + # No flows started + assert len(mock_config_entries.flow.mock_calls) == 0 + + # No configs stored + assert hass.data[hue.DOMAIN] == {} + + +async def test_setup_with_discovery_no_known_auth(hass, aioclient_mock): + """Test discovering a bridge and not having known auth.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + { + 'internalipaddress': '0.0.0.0', + 'id': 'abcd1234' + } + ]) + + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hue, 'configured_hosts', return_value=[]): + mock_config_entries.flow.async_init.return_value = mock_coro() + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: {} + }) is True + + # Flow started for discovered bridge + assert len(mock_config_entries.flow.mock_calls) == 1 + assert mock_config_entries.flow.mock_calls[0][2]['data'] == { + 'host': '0.0.0.0', + 'path': '.hue_abcd1234.conf', + } + + # Config stored for domain. + assert hass.data[hue.DOMAIN] == { + '0.0.0.0': { + hue.CONF_HOST: '0.0.0.0', + hue.CONF_FILENAME: '.hue_abcd1234.conf', + hue.CONF_ALLOW_HUE_GROUPS: hue.DEFAULT_ALLOW_HUE_GROUPS, + hue.CONF_ALLOW_UNREACHABLE: hue.DEFAULT_ALLOW_UNREACHABLE, + } + } + + +async def test_setup_with_discovery_known_auth(hass, aioclient_mock): + """Test we don't do anything if we discover already configured hub.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + { + 'internalipaddress': '0.0.0.0', + 'id': 'abcd1234' + } + ]) + + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hue, 'configured_hosts', return_value=['0.0.0.0']): + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: {} + }) is True + + # Flow started for discovered bridge + assert len(mock_config_entries.flow.mock_calls) == 0 + + # Config stored for domain. + assert hass.data[hue.DOMAIN] == {} + + +async def test_setup_defined_hosts_known_auth(hass): + """Test we don't initiate a config entry if config bridge is known.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hue, 'configured_hosts', return_value=['0.0.0.0']): + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: { + hue.CONF_BRIDGES: { + hue.CONF_HOST: '0.0.0.0', + hue.CONF_FILENAME: 'bla.conf', + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True + } + } + }) is True + + # Flow started for discovered bridge + assert len(mock_config_entries.flow.mock_calls) == 0 + + # Config stored for domain. + assert hass.data[hue.DOMAIN] == { + '0.0.0.0': { + hue.CONF_HOST: '0.0.0.0', + hue.CONF_FILENAME: 'bla.conf', + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True + } + } + + +async def test_setup_defined_hosts_no_known_auth(hass): + """Test we initiate config entry if config bridge is not known.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hue, 'configured_hosts', return_value=[]): + mock_config_entries.flow.async_init.return_value = mock_coro() + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: { + hue.CONF_BRIDGES: { + hue.CONF_HOST: '0.0.0.0', + hue.CONF_FILENAME: 'bla.conf', + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True + } + } + }) is True + + # Flow started for discovered bridge + assert len(mock_config_entries.flow.mock_calls) == 1 + assert mock_config_entries.flow.mock_calls[0][2]['data'] == { + 'host': '0.0.0.0', + 'path': 'bla.conf', + } + + # Config stored for domain. + assert hass.data[hue.DOMAIN] == { + '0.0.0.0': { + hue.CONF_HOST: '0.0.0.0', + hue.CONF_FILENAME: 'bla.conf', + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True + } + } + + +async def test_config_passed_to_config_entry(hass): + """Test that configured options for a host are loaded via config entry.""" + entry = MockConfigEntry(domain=hue.DOMAIN, data={ + 'host': '0.0.0.0', + }) + entry.add_to_hass(hass) + + with patch.object(hue, 'HueBridge') as mock_bridge: + mock_bridge.return_value.async_setup.return_value = mock_coro(True) + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: { + hue.CONF_BRIDGES: { + hue.CONF_HOST: '0.0.0.0', + hue.CONF_FILENAME: 'bla.conf', + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True + } + } + }) is True + + assert len(mock_bridge.mock_calls) == 2 + p_hass, p_entry, p_allow_unreachable, p_allow_groups = \ + mock_bridge.mock_calls[0][1] + + assert p_hass is hass + assert p_entry is entry + assert p_allow_unreachable is True + assert p_allow_groups is False diff --git a/tests/components/hue/test_setup.py b/tests/components/hue/test_setup.py deleted file mode 100644 index f90f58a50c3..00000000000 --- a/tests/components/hue/test_setup.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Test Hue setup process.""" -from homeassistant.setup import async_setup_component -from homeassistant.components import hue -from homeassistant.components.discovery import SERVICE_HUE - - -async def test_setup_with_multiple_hosts(hass, mock_bridge): - """Multiple hosts specified in the config file.""" - assert await async_setup_component(hass, hue.DOMAIN, { - hue.DOMAIN: { - hue.CONF_BRIDGES: [ - {hue.CONF_HOST: '127.0.0.1'}, - {hue.CONF_HOST: '192.168.1.10'}, - ] - } - }) - - assert len(mock_bridge.mock_calls) == 2 - hosts = sorted(mock_call[1][0] for mock_call in mock_bridge.mock_calls) - assert hosts == ['127.0.0.1', '192.168.1.10'] - - -async def test_bridge_discovered(hass, mock_bridge): - """Bridge discovery.""" - assert await async_setup_component(hass, hue.DOMAIN, {}) - - await hass.helpers.discovery.async_discover(SERVICE_HUE, { - 'host': '192.168.1.10', - 'serial': '1234567', - }) - await hass.async_block_till_done() - - assert len(mock_bridge.mock_calls) == 1 - assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' - - -async def test_bridge_configure_and_discovered(hass, mock_bridge): - """Bridge is in the config file, then we discover it.""" - assert await async_setup_component(hass, hue.DOMAIN, { - hue.DOMAIN: { - hue.CONF_BRIDGES: { - hue.CONF_HOST: '192.168.1.10' - } - } - }) - - assert len(mock_bridge.mock_calls) == 1 - assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' - hass.data[hue.DOMAIN] = {'192.168.1.10': {}} - - mock_bridge.reset_mock() - - await hass.helpers.discovery.async_discover(SERVICE_HUE, { - 'host': '192.168.1.10', - 'serial': '1234567', - }) - await hass.async_block_till_done() - - assert len(mock_bridge.mock_calls) == 0 - - -async def test_setup_no_host(hass, aioclient_mock): - """Check we call discovery if domain specified but no bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[]) - - result = await async_setup_component( - hass, hue.DOMAIN, {hue.DOMAIN: {}}) - assert result - - assert len(aioclient_mock.mock_calls) == 1 diff --git a/tests/components/test_discovery.py b/tests/components/test_discovery.py index 580d876982d..b4c80bf3210 100644 --- a/tests/components/test_discovery.py +++ b/tests/components/test_discovery.py @@ -5,6 +5,7 @@ from unittest.mock import patch, MagicMock import pytest +from homeassistant import config_entries from homeassistant.bootstrap import async_setup_component from homeassistant.components import discovery from homeassistant.util.dt import utcnow @@ -44,13 +45,12 @@ def netdisco_mock(): yield -@asyncio.coroutine -def mock_discovery(hass, discoveries, config=BASE_CONFIG): +async def mock_discovery(hass, discoveries, config=BASE_CONFIG): """Helper to mock discoveries.""" - result = yield from async_setup_component(hass, 'discovery', config) + result = await async_setup_component(hass, 'discovery', config) assert result - yield from hass.async_start() + await hass.async_start() with patch.object(discovery, '_discover', discoveries), \ patch('homeassistant.components.discovery.async_discover', @@ -59,8 +59,8 @@ def mock_discovery(hass, discoveries, config=BASE_CONFIG): return_value=mock_coro()) as mock_platform: async_fire_time_changed(hass, utcnow()) # Work around an issue where our loop.call_soon not get caught - yield from hass.async_block_till_done() - yield from hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() return mock_discover, mock_platform @@ -154,3 +154,25 @@ def test_load_component_hassio(hass): yield from mock_discovery(hass, discover) assert mock_hassio.called + + +async def test_discover_config_flow(hass): + """Test discovery triggering a config flow.""" + discovery_info = { + 'hello': 'world' + } + + def discover(netdisco): + """Fake discovery.""" + return [('mock-service', discovery_info)] + + with patch.dict(discovery.CONFIG_ENTRY_HANDLERS, { + 'mock-service': 'mock-component'}), patch( + 'homeassistant.config_entries.FlowManager.async_init') as m_init: + await mock_discovery(hass, discover) + + assert len(m_init.mock_calls) == 1 + args, kwargs = m_init.mock_calls[0][1:] + assert args == ('mock-component',) + assert kwargs['source'] == config_entries.SOURCE_DISCOVERY + assert kwargs['data'] == discovery_info From 5801d78017c6ef4cce986bc046db4d7eb448449c Mon Sep 17 00:00:00 2001 From: Albert Lee Date: Fri, 30 Mar 2018 01:49:08 -0500 Subject: [PATCH 060/136] Implement thermostat support for Alexa (#13340) * Implement thermostat support for Alexa * util.temperature: Support interval conversions * Use climate.ATTR_OPERATION_MODE for Alexa thermostat mode * Switch coroutines to async/await * Log all Alexa error events --- homeassistant/components/alexa/smart_home.py | 249 ++++++++++++++++++- homeassistant/util/temperature.py | 15 +- tests/components/alexa/test_smart_home.py | 127 ++++++++++ 3 files changed, 374 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 5e5155b3db8..707f8d02958 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -6,18 +6,20 @@ from datetime import datetime from uuid import uuid4 from homeassistant.components import ( - alert, automation, cover, fan, group, input_boolean, light, lock, + alert, automation, cover, climate, fan, group, input_boolean, light, lock, media_player, scene, script, switch, http, sensor) import homeassistant.core as ha import homeassistant.util.color as color_util +from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.decorator import Registry from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK, - SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, - SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME, + SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS, CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON) + from .const import CONF_FILTER, CONF_ENTITY_CONFIG _LOGGER = logging.getLogger(__name__) @@ -34,6 +36,16 @@ API_TEMP_UNITS = { TEMP_CELSIUS: 'CELSIUS', } +API_THERMOSTAT_MODES = { + climate.STATE_HEAT: 'HEAT', + climate.STATE_COOL: 'COOL', + climate.STATE_AUTO: 'AUTO', + climate.STATE_ECO: 'ECO', + climate.STATE_IDLE: 'OFF', + climate.STATE_FAN_ONLY: 'OFF', + climate.STATE_DRY: 'OFF', +} + SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home' CONF_DESCRIPTION = 'description' @@ -383,8 +395,60 @@ class _AlexaTemperatureSensor(_AlexaInterface): raise _UnsupportedProperty(name) unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT] + temp = self.entity.state + if self.entity.domain == climate.DOMAIN: + temp = self.entity.attributes.get( + climate.ATTR_CURRENT_TEMPERATURE) return { - 'value': float(self.entity.state), + 'value': float(temp), + 'scale': API_TEMP_UNITS[unit], + } + + +class _AlexaThermostatController(_AlexaInterface): + def name(self): + return 'Alexa.ThermostatController' + + def properties_supported(self): + properties = [] + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & climate.SUPPORT_TARGET_TEMPERATURE: + properties.append({'name': 'targetSetpoint'}) + if supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW: + properties.append({'name': 'lowerSetpoint'}) + if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH: + properties.append({'name': 'upperSetpoint'}) + if supported & climate.SUPPORT_OPERATION_MODE: + properties.append({'name': 'thermostatMode'}) + return properties + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name == 'thermostatMode': + ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE) + mode = API_THERMOSTAT_MODES.get(ha_mode) + if mode is None: + _LOGGER.error("%s (%s) has unsupported %s value '%s'", + self.entity.entity_id, type(self.entity), + climate.ATTR_OPERATION_MODE, ha_mode) + raise _UnsupportedProperty(name) + return mode + + unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT] + temp = None + if name == 'targetSetpoint': + temp = self.entity.attributes.get(ATTR_TEMPERATURE) + elif name == 'lowerSetpoint': + temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW) + elif name == 'upperSetpoint': + temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH) + if temp is None: + raise _UnsupportedProperty(name) + + return { + 'value': float(temp), 'scale': API_TEMP_UNITS[unit], } @@ -415,6 +479,16 @@ class _SwitchCapabilities(_AlexaEntity): return [_AlexaPowerController(self.entity)] +@ENTITY_ADAPTERS.register(climate.DOMAIN) +class _ClimateCapabilities(_AlexaEntity): + def default_display_categories(self): + return [_DisplayCategory.THERMOSTAT] + + def interfaces(self): + yield _AlexaThermostatController(self.entity) + yield _AlexaTemperatureSensor(self.entity) + + @ENTITY_ADAPTERS.register(cover.DOMAIN) class _CoverCapabilities(_AlexaEntity): def default_display_categories(self): @@ -682,17 +756,26 @@ def api_message(request, return response -def api_error(request, error_type='INTERNAL_ERROR', error_message=""): +def api_error(request, + namespace='Alexa', + error_type='INTERNAL_ERROR', + error_message="", + payload=None): """Create a API formatted error response. Async friendly. """ - payload = { - 'type': error_type, - 'message': error_message, - } + payload = payload or {} + payload['type'] = error_type + payload['message'] = error_message - return api_message(request, name='ErrorResponse', payload=payload) + _LOGGER.info("Request %s/%s error %s: %s", + request[API_HEADER]['namespace'], + request[API_HEADER]['name'], + error_type, error_message) + + return api_message( + request, name='ErrorResponse', namespace=namespace, payload=payload) @HANDLERS.register(('Alexa.Discovery', 'Discover')) @@ -1104,7 +1187,6 @@ def async_api_select_input(hass, config, request, entity): else: msg = 'failed to map input {} to a media source on {}'.format( media_input, entity.entity_id) - _LOGGER.error(msg) return api_error( request, error_type='INVALID_VALUE', error_message=msg) @@ -1276,6 +1358,149 @@ def async_api_previous(hass, config, request, entity): return api_message(request) +def api_error_temp_range(request, temp, min_temp, max_temp, unit): + """Create temperature value out of range API error response. + + Async friendly. + """ + temp_range = { + 'minimumValue': { + 'value': min_temp, + 'scale': API_TEMP_UNITS[unit], + }, + 'maximumValue': { + 'value': max_temp, + 'scale': API_TEMP_UNITS[unit], + }, + } + + msg = 'The requested temperature {} is out of range'.format(temp) + return api_error( + request, + error_type='TEMPERATURE_VALUE_OUT_OF_RANGE', + error_message=msg, + payload={'validRange': temp_range}, + ) + + +def temperature_from_object(temp_obj, to_unit, interval=False): + """Get temperature from Temperature object in requested unit.""" + from_unit = TEMP_CELSIUS + temp = float(temp_obj['value']) + + if temp_obj['scale'] == 'FAHRENHEIT': + from_unit = TEMP_FAHRENHEIT + elif temp_obj['scale'] == 'KELVIN': + # convert to Celsius if absolute temperature + if not interval: + temp -= 273.15 + + return convert_temperature(temp, from_unit, to_unit, interval) + + +@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature')) +@extract_entity +async def async_api_set_target_temp(hass, config, request, entity): + """Process a set target temperature request.""" + unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT] + min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) + max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP) + + data = { + ATTR_ENTITY_ID: entity.entity_id + } + + payload = request[API_PAYLOAD] + if 'targetSetpoint' in payload: + temp = temperature_from_object( + payload['targetSetpoint'], unit) + if temp < min_temp or temp > max_temp: + return api_error_temp_range( + request, temp, min_temp, max_temp, unit) + data[ATTR_TEMPERATURE] = temp + if 'lowerSetpoint' in payload: + temp_low = temperature_from_object( + payload['lowerSetpoint'], unit) + if temp_low < min_temp or temp_low > max_temp: + return api_error_temp_range( + request, temp_low, min_temp, max_temp, unit) + data[climate.ATTR_TARGET_TEMP_LOW] = temp_low + if 'upperSetpoint' in payload: + temp_high = temperature_from_object( + payload['upperSetpoint'], unit) + if temp_high < min_temp or temp_high > max_temp: + return api_error_temp_range( + request, temp_high, min_temp, max_temp, unit) + data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high + + await hass.services.async_call( + entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False) + + return api_message(request) + + +@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature')) +@extract_entity +async def async_api_adjust_target_temp(hass, config, request, entity): + """Process an adjust target temperature request.""" + unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT] + min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) + max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP) + + temp_delta = temperature_from_object( + request[API_PAYLOAD]['targetSetpointDelta'], unit, interval=True) + target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta + + if target_temp < min_temp or target_temp > max_temp: + return api_error_temp_range( + request, target_temp, min_temp, max_temp, unit) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + ATTR_TEMPERATURE: target_temp, + } + + await hass.services.async_call( + entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False) + + return api_message(request) + + +@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode')) +@extract_entity +async def async_api_set_thermostat_mode(hass, config, request, entity): + """Process a set thermostat mode request.""" + mode = request[API_PAYLOAD]['thermostatMode'] + + operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST) + # Work around a pylint false positive due to + # https://github.com/PyCQA/pylint/issues/1830 + # pylint: disable=stop-iteration-return + ha_mode = next( + (k for k, v in API_THERMOSTAT_MODES.items() if v == mode), + None + ) + if ha_mode not in operation_list: + msg = 'The requested thermostat mode {} is not supported'.format(mode) + return api_error( + request, + namespace='Alexa.ThermostatController', + error_type='UNSUPPORTED_THERMOSTAT_MODE', + error_message=msg + ) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + climate.ATTR_OPERATION_MODE: ha_mode, + } + + await hass.services.async_call( + entity.domain, climate.SERVICE_SET_OPERATION_MODE, data, + blocking=False) + + return api_message(request) + + @HANDLERS.register(('Alexa', 'ReportState')) @extract_entity @asyncio.coroutine diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index b7e2412f293..913d6456906 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -3,17 +3,22 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_NOT_RECOGNIZED_TEMPLATE, TEMPERATURE) -def fahrenheit_to_celsius(fahrenheit: float) -> float: +def fahrenheit_to_celsius(fahrenheit: float, interval: bool = False) -> float: """Convert a temperature in Fahrenheit to Celsius.""" + if interval: + return fahrenheit / 1.8 return (fahrenheit - 32.0) / 1.8 -def celsius_to_fahrenheit(celsius: float) -> float: +def celsius_to_fahrenheit(celsius: float, interval: bool = False) -> float: """Convert a temperature in Celsius to Fahrenheit.""" + if interval: + return celsius * 1.8 return celsius * 1.8 + 32.0 -def convert(temperature: float, from_unit: str, to_unit: str) -> float: +def convert(temperature: float, from_unit: str, to_unit: str, + interval: bool = False) -> float: """Convert a temperature from one unit to another.""" if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT): raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format( @@ -25,5 +30,5 @@ def convert(temperature: float, from_unit: str, to_unit: str) -> float: if from_unit == to_unit: return temperature elif from_unit == TEMP_CELSIUS: - return celsius_to_fahrenheit(temperature) - return fahrenheit_to_celsius(temperature) + return celsius_to_fahrenheit(temperature, interval) + return fahrenheit_to_celsius(temperature, interval) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 8199652d09e..dd404b7d57a 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -693,6 +693,133 @@ def test_unknown_sensor(hass): yield from discovery_test(device, hass, expected_endpoints=0) +async def test_thermostat(hass): + """Test thermostat discovery.""" + device = ( + 'climate.test_thermostat', + 'cool', + { + 'operation_mode': 'cool', + 'temperature': 70.0, + 'target_temp_high': 80.0, + 'target_temp_low': 60.0, + 'current_temperature': 75.0, + 'friendly_name': "Test Thermostat", + 'supported_features': 1 | 2 | 4 | 128, + 'operation_list': ['heat', 'cool', 'auto', 'off'], + 'min_temp': 50, + 'max_temp': 90, + 'unit_of_measurement': TEMP_FAHRENHEIT, + } + ) + appliance = await discovery_test(device, hass) + + assert appliance['endpointId'] == 'climate#test_thermostat' + assert appliance['displayCategories'][0] == 'THERMOSTAT' + assert appliance['friendlyName'] == "Test Thermostat" + + assert_endpoint_capabilities( + appliance, + 'Alexa.ThermostatController', + 'Alexa.TemperatureSensor', + ) + + properties = await reported_properties( + hass, 'climate#test_thermostat') + properties.assert_equal( + 'Alexa.ThermostatController', 'thermostatMode', 'COOL') + properties.assert_equal( + 'Alexa.ThermostatController', 'targetSetpoint', + {'value': 70.0, 'scale': 'FAHRENHEIT'}) + properties.assert_equal( + 'Alexa.TemperatureSensor', 'temperature', + {'value': 75.0, 'scale': 'FAHRENHEIT'}) + + call, _ = await assert_request_calls_service( + 'Alexa.ThermostatController', 'SetTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={'targetSetpoint': {'value': 69.0, 'scale': 'FAHRENHEIT'}} + ) + assert call.data['temperature'] == 69.0 + + msg = await assert_request_fails( + 'Alexa.ThermostatController', 'SetTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={'targetSetpoint': {'value': 0.0, 'scale': 'CELSIUS'}} + ) + assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE' + + call, _ = await assert_request_calls_service( + 'Alexa.ThermostatController', 'SetTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={ + 'targetSetpoint': {'value': 70.0, 'scale': 'FAHRENHEIT'}, + 'lowerSetpoint': {'value': 293.15, 'scale': 'KELVIN'}, + 'upperSetpoint': {'value': 30.0, 'scale': 'CELSIUS'}, + } + ) + assert call.data['temperature'] == 70.0 + assert call.data['target_temp_low'] == 68.0 + assert call.data['target_temp_high'] == 86.0 + + msg = await assert_request_fails( + 'Alexa.ThermostatController', 'SetTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={ + 'lowerSetpoint': {'value': 273.15, 'scale': 'KELVIN'}, + 'upperSetpoint': {'value': 75.0, 'scale': 'FAHRENHEIT'}, + } + ) + assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE' + + msg = await assert_request_fails( + 'Alexa.ThermostatController', 'SetTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={ + 'lowerSetpoint': {'value': 293.15, 'scale': 'FAHRENHEIT'}, + 'upperSetpoint': {'value': 75.0, 'scale': 'CELSIUS'}, + } + ) + assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE' + + call, _ = await assert_request_calls_service( + 'Alexa.ThermostatController', 'AdjustTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={'targetSetpointDelta': {'value': -10.0, 'scale': 'KELVIN'}} + ) + assert call.data['temperature'] == 52.0 + + msg = await assert_request_fails( + 'Alexa.ThermostatController', 'AdjustTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={'targetSetpointDelta': {'value': 20.0, 'scale': 'CELSIUS'}} + ) + assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE' + + call, _ = await assert_request_calls_service( + 'Alexa.ThermostatController', 'SetThermostatMode', + 'climate#test_thermostat', 'climate.set_operation_mode', + hass, + payload={'thermostatMode': 'HEAT'} + ) + assert call.data['operation_mode'] == 'heat' + + msg = await assert_request_fails( + 'Alexa.ThermostatController', 'SetThermostatMode', + 'climate#test_thermostat', 'climate.set_operation_mode', + hass, + payload={'thermostatMode': 'INVALID'} + ) + assert msg['event']['payload']['type'] == 'UNSUPPORTED_THERMOSTAT_MODE' + + @asyncio.coroutine def test_exclude_filters(hass): """Test exclusion filters.""" From 931bceefd99e29d4128ff5c2f6bf09b522e4cd8a Mon Sep 17 00:00:00 2001 From: Kane610 Date: Fri, 30 Mar 2018 09:34:26 +0200 Subject: [PATCH 061/136] deCONZ config entry (#13402) * Try config entries * Testing * Working flow * Config entry text strings * Removed manual inputs for config flow * Support unloading of config entry * Bump requirement to v33 * Fix comments from test * Make sure that only one deCONZ instance can be set up * Hass doesn't support unloading platforms yet * Modify get_api_key to be testable * Fix hound comments * Add test dependency * Add test for no key * Bump requirement to v35 Add pydeconz to list of test components * Don't have a check in async_setup that domain exists in hass.data --- .../components/deconz/.translations/en.json | 25 +++++ homeassistant/components/deconz/__init__.py | 90 ++++++++++++++++- homeassistant/components/deconz/strings.json | 25 +++++ homeassistant/config_entries.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/test_deconz.py | 97 +++++++++++++++++++ 8 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/deconz/.translations/en.json create mode 100644 homeassistant/components/deconz/strings.json create mode 100644 tests/components/test_deconz.py diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json new file mode 100644 index 00000000000..69165dbbbaf --- /dev/null +++ b/homeassistant/components/deconz/.translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "title": "deCONZ", + "step": { + "init": { + "title": "Define deCONZ gateway", + "data": { + "host": "Host", + "port": "Port (default value: '80')" + } + }, + "link": { + "title": "Link with deCONZ", + "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" + } + }, + "error": { + "no_key": "Couldn't get an API key" + }, + "abort": { + "no_bridges": "No deCONZ bridges discovered", + "one_instance_only": "Component only supports one deCONZ instance" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 26d9fb401e4..85ba271ec3a 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -8,16 +8,17 @@ import logging import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.discovery import SERVICE_DECONZ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import discovery, aiohttp_client from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pydeconz==32'] +REQUIREMENTS = ['pydeconz==35'] _LOGGER = logging.getLogger(__name__) @@ -160,7 +161,8 @@ async def async_request_configuration(hass, config, deconz_config): async def async_configuration_callback(data): """Set up actions to do when our configuration callback is called.""" from pydeconz.utils import async_get_api_key - api_key = await async_get_api_key(hass.loop, **deconz_config) + websession = async_get_clientsession(hass) + api_key = await async_get_api_key(websession, **deconz_config) if api_key: deconz_config[CONF_API_KEY] = api_key result = await async_setup_deconz(hass, config, deconz_config) @@ -186,3 +188,85 @@ async def async_request_configuration(hass, config, deconz_config): entity_picture="/static/images/logo_deconz.jpeg", submit_caption="I have unlocked the gateway", ) + + +@config_entries.HANDLERS.register(DOMAIN) +class DeconzFlowHandler(config_entries.ConfigFlowHandler): + """Handle a deCONZ config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize the deCONZ flow.""" + self.bridges = [] + self.deconz_config = {} + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + from pydeconz.utils import async_discovery + + if DOMAIN in self.hass.data: + return self.async_abort( + reason='one_instance_only' + ) + + if user_input is not None: + for bridge in self.bridges: + if bridge[CONF_HOST] == user_input[CONF_HOST]: + self.deconz_config = bridge + return await self.async_step_link() + + session = aiohttp_client.async_get_clientsession(self.hass) + self.bridges = await async_discovery(session) + + if len(self.bridges) == 1: + self.deconz_config = self.bridges[0] + return await self.async_step_link() + elif len(self.bridges) > 1: + hosts = [] + for bridge in self.bridges: + hosts.append(bridge[CONF_HOST]) + return self.async_show_form( + step_id='init', + data_schema=vol.Schema({ + vol.Required(CONF_HOST): vol.In(hosts) + }) + ) + + return self.async_abort( + reason='no_bridges' + ) + + async def async_step_link(self, user_input=None): + """Attempt to link with the deCONZ bridge.""" + from pydeconz.utils import async_get_api_key + errors = {} + + if user_input is not None: + session = aiohttp_client.async_get_clientsession(self.hass) + api_key = await async_get_api_key(session, **self.deconz_config) + if api_key: + self.deconz_config[CONF_API_KEY] = api_key + return self.async_create_entry( + title='deCONZ', + data=self.deconz_config + ) + else: + errors['base'] = 'no_key' + + return self.async_show_form( + step_id='link', + errors=errors, + ) + + +async def async_setup_entry(hass, entry): + """Set up a bridge for a config entry.""" + if DOMAIN in hass.data: + _LOGGER.error( + "Config entry failed since one deCONZ instance already exists") + return False + result = await async_setup_deconz(hass, None, entry.data) + if result: + return True + return False diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json new file mode 100644 index 00000000000..69165dbbbaf --- /dev/null +++ b/homeassistant/components/deconz/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "title": "deCONZ", + "step": { + "init": { + "title": "Define deCONZ gateway", + "data": { + "host": "Host", + "port": "Port (default value: '80')" + } + }, + "link": { + "title": "Link with deCONZ", + "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" + } + }, + "error": { + "no_key": "Couldn't get an API key" + }, + "abort": { + "no_bridges": "No deCONZ bridges discovered", + "one_instance_only": "Component only supports one deCONZ instance" + } + } +} \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b02026ac6dd..6b2000b2ea6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -128,6 +128,7 @@ HANDLERS = Registry() FLOWS = [ 'config_entry_example', 'hue', + 'deconz', ] SOURCE_USER = 'user' diff --git a/requirements_all.txt b/requirements_all.txt index b21452dc385..676945eb42e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -714,7 +714,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==32 +pydeconz==35 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31a7874409a..456bec7d6a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -129,6 +129,9 @@ pushbullet.py==0.11.0 # homeassistant.components.canary py-canary==0.4.1 +# homeassistant.components.deconz +pydeconz==35 + # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 1f5348136c6..fa39c307f18 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -67,6 +67,7 @@ TEST_REQUIREMENTS = ( 'prometheus_client', 'pushbullet.py', 'py-canary', + 'pydeconz', 'pydispatcher', 'PyJWT', 'pylitejet', diff --git a/tests/components/test_deconz.py b/tests/components/test_deconz.py new file mode 100644 index 00000000000..2c7c656d560 --- /dev/null +++ b/tests/components/test_deconz.py @@ -0,0 +1,97 @@ +"""Tests for deCONZ config flow.""" +import pytest + +import voluptuous as vol + +import homeassistant.components.deconz as deconz +import pydeconz + + +async def test_flow_works(hass, aioclient_mock): + """Test config flow.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ + {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': '80'} + ]) + aioclient_mock.post('http://1.2.3.4:80/api', json=[ + {"success": {"username": "1234567890ABCDEF"}} + ]) + + flow = deconz.DeconzFlowHandler() + flow.hass = hass + await flow.async_step_init() + result = await flow.async_step_link(user_input={}) + + assert result['type'] == 'create_entry' + assert result['title'] == 'deCONZ' + assert result['data'] == { + 'bridgeid': 'id', + 'host': '1.2.3.4', + 'port': '80', + 'api_key': '1234567890ABCDEF' + } + + +async def test_flow_already_registered_bridge(hass, aioclient_mock): + """Test config flow don't allow more than one bridge to be registered.""" + flow = deconz.DeconzFlowHandler() + flow.hass = hass + flow.hass.data[deconz.DOMAIN] = True + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_no_discovered_bridges(hass, aioclient_mock): + """Test config flow discovers no bridges.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[]) + flow = deconz.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_one_bridge_discovered(hass, aioclient_mock): + """Test config flow discovers one bridge.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ + {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': '80'} + ]) + flow = deconz.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_flow_two_bridges_discovered(hass, aioclient_mock): + """Test config flow discovers two bridges.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ + {'id': 'id1', 'internalipaddress': '1.2.3.4', 'internalport': '80'}, + {'id': 'id2', 'internalipaddress': '5.6.7.8', 'internalport': '80'} + ]) + flow = deconz.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'init' + + with pytest.raises(vol.Invalid): + assert result['data_schema']({'host': '0.0.0.0'}) + + result['data_schema']({'host': '1.2.3.4'}) + result['data_schema']({'host': '5.6.7.8'}) + + +async def test_flow_no_api_key(hass, aioclient_mock): + """Test config flow discovers no bridges.""" + aioclient_mock.post('http://1.2.3.4:80/api', json=[]) + flow = deconz.DeconzFlowHandler() + flow.hass = hass + flow.deconz_config = {'host': '1.2.3.4', 'port': 80} + + result = await flow.async_step_link(user_input={}) + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert result['errors'] == {'base': 'no_key'} From 6314aabc6fba709fe7c09b8ef46070099cb3d9df Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 30 Mar 2018 16:16:29 +0300 Subject: [PATCH 062/136] Remove andrey-git from requirements monitoring (#13547) --- CODEOWNERS | 3 --- 1 file changed, 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9528e7a09e9..932f07573b2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -29,9 +29,6 @@ homeassistant/components/weblink.py @home-assistant/core homeassistant/components/websocket_api.py @home-assistant/core homeassistant/components/zone.py @home-assistant/core -# To monitor non-pypi additions -requirements_all.txt @andrey-git - # HomeAssistant developer Teams Dockerfile @home-assistant/docker virtualization/Docker/* @home-assistant/docker From 979a8f87728b23ffeef78159deb1fa6570ac628a Mon Sep 17 00:00:00 2001 From: ChristianKuehnel Date: Fri, 30 Mar 2018 18:12:57 +0200 Subject: [PATCH 063/136] Fix BMW device tracker toggling state if vehicle tracking is disabled (#12999) * if tracking is disabled, the position is not set in the device tracker. This fixes an issue with a toggling vehicle state. * removed useless attributes --- .../device_tracker/bmw_connected_drive.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/device_tracker/bmw_connected_drive.py b/homeassistant/components/device_tracker/bmw_connected_drive.py index 1e501c0e199..2267bb51944 100644 --- a/homeassistant/components/device_tracker/bmw_connected_drive.py +++ b/homeassistant/components/device_tracker/bmw_connected_drive.py @@ -36,16 +36,20 @@ class BMWDeviceTracker(object): self.vehicle = vehicle def update(self) -> None: - """Update the device info.""" + """Update the device info. + + Only update the state in home assistant if tracking in + the car is enabled. + """ dev_id = slugify(self.vehicle.name) + + if not self.vehicle.state.is_vehicle_tracking_enabled: + _LOGGER.debug('Tracking is disabled for vehicle %s', dev_id) + return + _LOGGER.debug('Updating %s', dev_id) - attrs = { - 'trackr_id': dev_id, - 'id': dev_id, - 'name': self.vehicle.name - } + self._see( dev_id=dev_id, host_name=self.vehicle.name, - gps=self.vehicle.state.gps_position, attributes=attrs, - icon='mdi:car' + gps=self.vehicle.state.gps_position, icon='mdi:car' ) From 9cfcd38c1e561c2c195a98babf403bcd4db68fca Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 30 Mar 2018 21:02:02 +0200 Subject: [PATCH 064/136] Xiaomi MiIO Switch: Support for the Xiaomi Chuangmi Plug V3 (#13271) * Device support of the Xiaomi Chuangmi Plug V3 added * Refactoring. * Additional attributes added. * New miio device class used --- .../components/switch/xiaomi_miio.py | 101 ++++++++++-------- 1 file changed, 59 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 6110b6dc469..149acd76c07 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -24,6 +24,7 @@ DATA_KEY = 'switch.xiaomi_miio' CONF_MODEL = 'model' MODEL_POWER_STRIP_V2 = 'zimi.powerstrip.v2' +MODEL_PLUG_V3 = 'chuangmi.plug.v3' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -34,7 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'qmi.powerstrip.v1', 'zimi.powerstrip.v2', 'chuangmi.plug.m1', - 'chuangmi.plug.v2']), + 'chuangmi.plug.v2', + 'chuangmi.plug.v3']), }) REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] @@ -51,18 +53,20 @@ ATTR_PRICE = 'price' SUCCESS = ['ok'] -SUPPORT_SET_POWER_MODE = 1 -SUPPORT_SET_WIFI_LED = 2 -SUPPORT_SET_POWER_PRICE = 4 +FEATURE_SET_POWER_MODE = 1 +FEATURE_SET_WIFI_LED = 2 +FEATURE_SET_POWER_PRICE = 4 -ADDITIONAL_SUPPORT_FLAGS_GENERIC = 0 +FEATURE_FLAGS_GENERIC = 0 -ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V1 = (SUPPORT_SET_POWER_MODE | - SUPPORT_SET_WIFI_LED | - SUPPORT_SET_POWER_PRICE) +FEATURE_FLAGS_POWER_STRIP_V1 = (FEATURE_SET_POWER_MODE | + FEATURE_SET_WIFI_LED | + FEATURE_SET_POWER_PRICE) -ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V2 = (SUPPORT_SET_WIFI_LED | - SUPPORT_SET_POWER_PRICE) +FEATURE_FLAGS_POWER_STRIP_V2 = (FEATURE_SET_WIFI_LED | + FEATURE_SET_POWER_PRICE) + +FEATURE_FLAGS_PLUG_V3 = (FEATURE_SET_WIFI_LED) SERVICE_SET_WIFI_LED_ON = 'xiaomi_miio_set_wifi_led_on' SERVICE_SET_WIFI_LED_OFF = 'xiaomi_miio_set_wifi_led_off' @@ -124,29 +128,27 @@ async def async_setup_platform(hass, config, async_add_devices, except DeviceException: raise PlatformNotReady - if model in ['chuangmi.plug.v1']: - from miio import PlugV1 - plug = PlugV1(host, token) + if model in ['chuangmi.plug.v1', 'chuangmi.plug.v3']: + from miio import ChuangmiPlug + plug = ChuangmiPlug(host, token, model=model) # The device has two switchable channels (mains and a USB port). # A switch device per channel will be created. for channel_usb in [True, False]: - device = ChuangMiPlugV1Switch( + device = ChuangMiPlugSwitch( name, plug, model, unique_id, channel_usb) devices.append(device) hass.data[DATA_KEY][host] = device - elif model in ['qmi.powerstrip.v1', - 'zimi.powerstrip.v2']: + elif model in ['qmi.powerstrip.v1', 'zimi.powerstrip.v2']: from miio import PowerStrip plug = PowerStrip(host, token) device = XiaomiPowerStripSwitch(name, plug, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device - elif model in ['chuangmi.plug.m1', - 'chuangmi.plug.v2']: - from miio import Plug - plug = Plug(host, token) + elif model in ['chuangmi.plug.m1', 'chuangmi.plug.v2']: + from miio import ChuangmiPlug + plug = ChuangmiPlug(host, token, model=model) device = XiaomiPlugGenericSwitch(name, plug, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device @@ -204,7 +206,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice): ATTR_TEMPERATURE: None, ATTR_MODEL: self._model, } - self._additional_supported_features = ADDITIONAL_SUPPORT_FLAGS_GENERIC + self._device_features = FEATURE_FLAGS_GENERIC self._skip_update = False @property @@ -251,6 +253,10 @@ class XiaomiPlugGenericSwitch(SwitchDevice): _LOGGER.debug("Response received from plug: %s", result) + # The Chuangmi Plug V3 returns 0 on success on usb_on/usb_off. + if func in ['usb_on', 'usb_off'] and result == 0: + return True + return result == SUCCESS except DeviceException as exc: _LOGGER.error(mask_error, exc) @@ -300,7 +306,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice): async def async_set_wifi_led_on(self): """Turn the wifi led on.""" - if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 0: + if self._device_features & FEATURE_SET_WIFI_LED == 0: return await self._try_command( @@ -309,7 +315,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice): async def async_set_wifi_led_off(self): """Turn the wifi led on.""" - if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 0: + if self._device_features & FEATURE_SET_WIFI_LED == 0: return await self._try_command( @@ -318,7 +324,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice): async def async_set_power_price(self, price: int): """Set the power price.""" - if self._additional_supported_features & SUPPORT_SET_POWER_PRICE == 0: + if self._device_features & FEATURE_SET_POWER_PRICE == 0: return await self._try_command( @@ -331,26 +337,24 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): def __init__(self, name, plug, model, unique_id): """Initialize the plug switch.""" - XiaomiPlugGenericSwitch.__init__(self, name, plug, model, unique_id) + super().__init__(name, plug, model, unique_id) if self._model == MODEL_POWER_STRIP_V2: - self._additional_supported_features = \ - ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V2 + self._device_features = FEATURE_FLAGS_POWER_STRIP_V2 else: - self._additional_supported_features = \ - ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V1 + self._device_features = FEATURE_FLAGS_POWER_STRIP_V1 self._state_attrs.update({ ATTR_LOAD_POWER: None, }) - if self._additional_supported_features & SUPPORT_SET_POWER_MODE == 1: + if self._device_features & FEATURE_SET_POWER_MODE == 1: self._state_attrs[ATTR_POWER_MODE] = None - if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 1: + if self._device_features & FEATURE_SET_WIFI_LED == 1: self._state_attrs[ATTR_WIFI_LED] = None - if self._additional_supported_features & SUPPORT_SET_POWER_PRICE == 1: + if self._device_features & FEATURE_SET_POWER_PRICE == 1: self._state_attrs[ATTR_POWER_PRICE] = None async def async_update(self): @@ -373,16 +377,16 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): ATTR_LOAD_POWER: state.load_power, }) - if self._additional_supported_features & \ - SUPPORT_SET_POWER_MODE == 1 and state.mode: + if self._device_features & FEATURE_SET_POWER_MODE == 1 and \ + state.mode: self._state_attrs[ATTR_POWER_MODE] = state.mode.value - if self._additional_supported_features & \ - SUPPORT_SET_WIFI_LED == 1 and state.wifi_led: + if self._device_features & FEATURE_SET_WIFI_LED == 1 and \ + state.wifi_led: self._state_attrs[ATTR_WIFI_LED] = state.wifi_led - if self._additional_supported_features & \ - SUPPORT_SET_POWER_PRICE == 1 and state.power_price: + if self._device_features & FEATURE_SET_POWER_PRICE == 1 and \ + state.power_price: self._state_attrs[ATTR_POWER_PRICE] = state.power_price except DeviceException as ex: @@ -391,7 +395,7 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): async def async_set_power_mode(self, mode: str): """Set the power mode.""" - if self._additional_supported_features & SUPPORT_SET_POWER_MODE == 0: + if self._device_features & FEATURE_SET_POWER_MODE == 0: return from miio.powerstrip import PowerMode @@ -401,8 +405,8 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): self._plug.set_power_mode, PowerMode(mode)) -class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch): - """Representation of a Chuang Mi Plug V1.""" +class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): + """Representation of a Chuang Mi Plug V1 and V3.""" def __init__(self, name, plug, model, unique_id, channel_usb): """Initialize the plug switch.""" @@ -411,9 +415,16 @@ class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch): if unique_id is not None and channel_usb: unique_id = "{}-{}".format(unique_id, 'usb') - XiaomiPlugGenericSwitch.__init__(self, name, plug, model, unique_id) + super().__init__(name, plug, model, unique_id) self._channel_usb = channel_usb + if self._model == MODEL_PLUG_V3: + self._device_features = FEATURE_FLAGS_PLUG_V3 + self._state_attrs.update({ + ATTR_WIFI_LED: None, + ATTR_LOAD_POWER: None, + }) + async def async_turn_on(self, **kwargs): """Turn a channel on.""" if self._channel_usb: @@ -463,6 +474,12 @@ class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch): ATTR_TEMPERATURE: state.temperature }) + if state.wifi_led: + self._state_attrs[ATTR_WIFI_LED] = state.wifi_led + + if state.load_power: + self._state_attrs[ATTR_LOAD_POWER] = state.load_power + except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) From 8fad97a47a7e6ca996a753da1b0ed31cc7005867 Mon Sep 17 00:00:00 2001 From: Beat <508289+bdurrer@users.noreply.github.com> Date: Fri, 30 Mar 2018 21:33:30 +0200 Subject: [PATCH 065/136] Add FreeDNS component (#13526) * Add FreeDNS component * Implement review changes in FreeDNS component * Implement review changes in FreeDNS component * Implement review changes in FreeDNS component --- homeassistant/components/freedns.py | 103 ++++++++++++++++++++++++++++ tests/components/test_freedns.py | 69 +++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 homeassistant/components/freedns.py create mode 100644 tests/components/test_freedns.py diff --git a/homeassistant/components/freedns.py b/homeassistant/components/freedns.py new file mode 100644 index 00000000000..0512030bdcb --- /dev/null +++ b/homeassistant/components/freedns.py @@ -0,0 +1,103 @@ +""" +Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/freedns/ +""" +import asyncio +from datetime import timedelta +import logging + +import aiohttp +import async_timeout +import voluptuous as vol + +from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'freedns' + +DEFAULT_INTERVAL = timedelta(minutes=10) + +TIMEOUT = 10 +UPDATE_URL = 'https://freedns.afraid.org/dynamic/update.php' + +CONF_UPDATE_INTERVAL = 'update_interval' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Exclusive(CONF_URL, DOMAIN): cv.string, + vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): vol.All( + cv.time_period, cv.positive_timedelta), + + }) +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup(hass, config): + """Initialize the FreeDNS component.""" + url = config[DOMAIN].get(CONF_URL) + auth_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) + update_interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL) + + session = hass.helpers.aiohttp_client.async_get_clientsession() + + result = yield from _update_freedns( + hass, session, url, auth_token) + + if result is False: + return False + + @asyncio.coroutine + def update_domain_callback(now): + """Update the FreeDNS entry.""" + yield from _update_freedns(hass, session, url, auth_token) + + hass.helpers.event.async_track_time_interval( + update_domain_callback, update_interval) + + return True + + +@asyncio.coroutine +def _update_freedns(hass, session, url, auth_token): + """Update FreeDNS.""" + params = None + + if url is None: + url = UPDATE_URL + + if auth_token is not None: + params = {} + params[auth_token] = "" + + try: + with async_timeout.timeout(TIMEOUT, loop=hass.loop): + resp = yield from session.get(url, params=params) + body = yield from resp.text() + + if "has not changed" in body: + # IP has not changed. + _LOGGER.debug("FreeDNS update skipped: IP has not changed") + return True + + if "ERROR" not in body: + _LOGGER.debug("Updating FreeDNS was successful: %s", body) + return True + + if "Invalid update URL" in body: + _LOGGER.error("FreeDNS update token is invalid") + else: + _LOGGER.warning("Updating FreeDNS failed: %s", body) + + except aiohttp.ClientError: + _LOGGER.warning("Can't connect to FreeDNS API") + + except asyncio.TimeoutError: + _LOGGER.warning("Timeout from FreeDNS API at %s", url) + + return False diff --git a/tests/components/test_freedns.py b/tests/components/test_freedns.py new file mode 100644 index 00000000000..b8e38e9c3a8 --- /dev/null +++ b/tests/components/test_freedns.py @@ -0,0 +1,69 @@ +"""Test the FreeDNS component.""" +import asyncio +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components import freedns +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed + +ACCESS_TOKEN = 'test_token' +UPDATE_INTERVAL = freedns.DEFAULT_INTERVAL +UPDATE_URL = freedns.UPDATE_URL + + +@pytest.fixture +def setup_freedns(hass, aioclient_mock): + """Fixture that sets up FreeDNS.""" + params = {} + params[ACCESS_TOKEN] = "" + aioclient_mock.get( + UPDATE_URL, params=params, text='Successfully updated 1 domains.') + + hass.loop.run_until_complete(async_setup_component(hass, freedns.DOMAIN, { + freedns.DOMAIN: { + 'access_token': ACCESS_TOKEN, + 'update_interval': UPDATE_INTERVAL, + } + })) + + +@asyncio.coroutine +def test_setup(hass, aioclient_mock): + """Test setup works if update passes.""" + params = {} + params[ACCESS_TOKEN] = "" + aioclient_mock.get( + UPDATE_URL, params=params, text='ERROR: Address has not changed.') + + result = yield from async_setup_component(hass, freedns.DOMAIN, { + freedns.DOMAIN: { + 'access_token': ACCESS_TOKEN, + 'update_interval': UPDATE_INTERVAL, + } + }) + assert result + assert aioclient_mock.call_count == 1 + + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) + yield from hass.async_block_till_done() + assert aioclient_mock.call_count == 2 + + +@asyncio.coroutine +def test_setup_fails_if_wrong_token(hass, aioclient_mock): + """Test setup fails if first update fails through wrong token.""" + params = {} + params[ACCESS_TOKEN] = "" + aioclient_mock.get( + UPDATE_URL, params=params, text='ERROR: Invalid update URL (2)') + + result = yield from async_setup_component(hass, freedns.DOMAIN, { + freedns.DOMAIN: { + 'access_token': ACCESS_TOKEN, + 'update_interval': UPDATE_INTERVAL, + } + }) + assert not result + assert aioclient_mock.call_count == 1 From 0911166c9c89d82a957bc177deb40bc3245830b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 30 Mar 2018 22:34:16 +0300 Subject: [PATCH 066/136] Update pylint to 1.8.3 (#13544) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index afcdec23a00..38b716406fd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.580 pydocstyle==1.1.1 -pylint==1.8.2 +pylint==1.8.3 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 456bec7d6a8..a6b655e95f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -9,7 +9,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.580 pydocstyle==1.1.1 -pylint==1.8.2 +pylint==1.8.3 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 From 0f24fea6bbbbeda45e4608eed3af2ed0add7408b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Fri, 30 Mar 2018 22:47:20 +0200 Subject: [PATCH 067/136] Google Maps location sharing device tracker (#12301) * Google Maps location sharing device tracker. * Use ConfigType and change debug logging to _LOGGER.debug() * Update to locationsharinglib 0.3.0 * Remove unneeded lines. * Use hass.config.path for config file location. * Fixed remarks * Return boolean in setup_scanner --- .coveragerc | 1 + .../components/device_tracker/google_maps.py | 83 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 87 insertions(+) create mode 100644 homeassistant/components/device_tracker/google_maps.py diff --git a/.coveragerc b/.coveragerc index d6cc126ef52..65f29767673 100644 --- a/.coveragerc +++ b/.coveragerc @@ -374,6 +374,7 @@ omit = homeassistant/components/device_tracker/cisco_ios.py homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/fritz.py + homeassistant/components/device_tracker/google_maps.py homeassistant/components/device_tracker/gpslogger.py homeassistant/components/device_tracker/hitron_coda.py homeassistant/components/device_tracker/huawei_router.py diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py new file mode 100644 index 00000000000..9e257616361 --- /dev/null +++ b/homeassistant/components/device_tracker/google_maps.py @@ -0,0 +1,83 @@ +""" +Support for Google Maps location sharing. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.google_maps/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA, SOURCE_TYPE_GPS) +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['locationsharinglib==0.4.0'] + +CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) + + +def setup_scanner(hass, config: ConfigType, see, discovery_info=None): + """Set up the scanner.""" + scanner = GoogleMapsScanner(hass, config, see) + return scanner.success_init + + +class GoogleMapsScanner(object): + """Representation of an Google Maps location sharing account.""" + + def __init__(self, hass, config: ConfigType, see) -> None: + """Initialize the scanner.""" + from locationsharinglib import Service + from locationsharinglib.locationsharinglibexceptions import InvalidUser + + self.see = see + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + try: + self.service = Service(self.username, self.password, + hass.config.path(CREDENTIALS_FILE)) + self._update_info() + + track_time_interval( + hass, self._update_info, MIN_TIME_BETWEEN_SCANS) + + self.success_init = True + + except InvalidUser: + _LOGGER.error('You have specified invalid login credentials') + self.success_init = False + + def _update_info(self, now=None): + for person in self.service.get_all_people(): + dev_id = 'google_maps_{0}'.format(slugify(person.id)) + + attrs = { + 'id': person.id, + 'nickname': person.nickname, + 'full_name': person.full_name, + 'last_seen': person.datetime, + 'address': person.address + } + self.see( + dev_id=dev_id, + gps=(person.latitude, person.longitude), + picture=person.picture_url, + source_type=SOURCE_TYPE_GPS, + attributes=attrs + ) diff --git a/requirements_all.txt b/requirements_all.txt index 676945eb42e..bd0a55d7c43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -480,6 +480,9 @@ liveboxplaytv==2.0.2 # homeassistant.components.notify.lametric lmnotify==0.0.4 +# homeassistant.components.device_tracker.google_maps +locationsharinglib==0.4.0 + # homeassistant.components.sensor.luftdaten luftdaten==0.1.3 From c361b0c4500bec29e79fb6b031d09bed71695665 Mon Sep 17 00:00:00 2001 From: Jonas Skoogh Date: Fri, 30 Mar 2018 22:50:08 +0200 Subject: [PATCH 068/136] Check_config: Handle numbers correctly when printing config (#13377) --- homeassistant/scripts/check_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index ac3ac62e82d..8c78602f3d0 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -252,7 +252,7 @@ def dump_dict(layer, indent_count=3, listi=False, **kwargs): """ def sort_dict_key(val): """Return the dict key for sorting.""" - key = str.lower(val[0]) + key = str(val[0]).lower() return '0' if key == 'platform' else key indent_str = indent_count * ' ' @@ -261,10 +261,10 @@ def dump_dict(layer, indent_count=3, listi=False, **kwargs): if isinstance(layer, Dict): for key, value in sorted(layer.items(), key=sort_dict_key): if isinstance(value, (dict, list)): - print(indent_str, key + ':', line_info(value, **kwargs)) + print(indent_str, str(key) + ':', line_info(value, **kwargs)) dump_dict(value, indent_count + 2) else: - print(indent_str, key + ':', value) + print(indent_str, str(key) + ':', value) indent_str = indent_count * ' ' if isinstance(layer, Sequence): for i in layer: From f40efe0110e8638987a0447aa81392940d7ffd78 Mon Sep 17 00:00:00 2001 From: dramamoose Date: Fri, 30 Mar 2018 15:10:25 -0600 Subject: [PATCH 069/136] Fix FLUX_LED error when no color is set (#13527) * Handle turn_on situation when no color is set As is, an error gets thrown when turn_on is called without an HS value. By adding an if statement, we only try to set RGB if an HS value is applied. * Fix Whitespace Issues * Made Requested Changes --- homeassistant/components/light/flux_led.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index ed0836f1449..6ffdcc0bb4a 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -204,7 +204,12 @@ class FluxLight(Light): self._bulb.turnOn() hs_color = kwargs.get(ATTR_HS_COLOR) - rgb = color_util.color_hs_to_RGB(*hs_color) + + if hs_color: + rgb = color_util.color_hs_to_RGB(*hs_color) + else: + rgb = None + brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) From ad5a11ba3d379c414bf4e2cb3d0d0529ea7d779e Mon Sep 17 00:00:00 2001 From: Joe Lu Date: Fri, 30 Mar 2018 14:38:29 -0700 Subject: [PATCH 070/136] Add support for Canary Flex (#13280) Add support for Canary Flex --- homeassistant/components/canary.py | 2 +- homeassistant/components/sensor/canary.py | 22 +++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensor/test_canary.py | 57 +++++++++++++++++++---- tests/components/test_canary.py | 6 ++- 6 files changed, 72 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/canary.py b/homeassistant/components/canary.py index 03825bf48a9..4d0fbe617b2 100644 --- a/homeassistant/components/canary.py +++ b/homeassistant/components/canary.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT from homeassistant.helpers import discovery from homeassistant.util import Throttle -REQUIREMENTS = ['py-canary==0.4.1'] +REQUIREMENTS = ['py-canary==0.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/canary.py b/homeassistant/components/sensor/canary.py index ded8f36203e..51fe1d4dd7a 100644 --- a/homeassistant/components/sensor/canary.py +++ b/homeassistant/components/sensor/canary.py @@ -8,6 +8,7 @@ https://home-assistant.io/components/sensor.canary/ from homeassistant.components.canary import DATA_CANARY from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level DEPENDENCIES = ['canary'] @@ -17,9 +18,11 @@ ATTR_AIR_QUALITY = "air_quality" # Sensor types are defined like so: # sensor type name, unit_of_measurement, icon SENSOR_TYPES = [ - ["temperature", TEMP_CELSIUS, "mdi:thermometer"], - ["humidity", "%", "mdi:water-percent"], - ["air_quality", None, "mdi:weather-windy"], + ["temperature", TEMP_CELSIUS, "mdi:thermometer", ["Canary"]], + ["humidity", "%", "mdi:water-percent", ["Canary"]], + ["air_quality", None, "mdi:weather-windy", ["Canary"]], + ["wifi", "dBm", "mdi:wifi", ["Canary Flex"]], + ["battery", "%", "mdi:battery-50", ["Canary Flex"]], ] STATE_AIR_QUALITY_NORMAL = "normal" @@ -35,9 +38,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for location in data.locations: for device in location.devices: if device.is_online: + device_type = device.device_type for sensor_type in SENSOR_TYPES: - devices.append(CanarySensor(data, sensor_type, location, - device)) + if device_type.get("name") in sensor_type[3]: + devices.append(CanarySensor(data, sensor_type, + location, device)) add_devices(devices, True) @@ -80,6 +85,9 @@ class CanarySensor(Entity): @property def icon(self): """Icon for the sensor.""" + if self.state is not None and self._sensor_type[0] == "battery": + return icon_for_battery_level(battery_level=self.state) + return self._sensor_type[2] @property @@ -113,6 +121,10 @@ class CanarySensor(Entity): canary_sensor_type = SensorType.TEMPERATURE elif self._sensor_type[0] == "humidity": canary_sensor_type = SensorType.HUMIDITY + elif self._sensor_type[0] == "wifi": + canary_sensor_type = SensorType.WIFI + elif self._sensor_type[0] == "battery": + canary_sensor_type = SensorType.BATTERY value = self._data.get_reading(self._device_id, canary_sensor_type) diff --git a/requirements_all.txt b/requirements_all.txt index bd0a55d7c43..ccdb5fc5669 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ pwmled==1.2.1 py-august==0.4.0 # homeassistant.components.canary -py-canary==0.4.1 +py-canary==0.5.0 # homeassistant.components.sensor.cpuspeed py-cpuinfo==3.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6b655e95f8..ef28e3a25e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -127,7 +127,7 @@ prometheus_client==0.1.0 pushbullet.py==0.11.0 # homeassistant.components.canary -py-canary==0.4.1 +py-canary==0.5.0 # homeassistant.components.deconz pydeconz==35 diff --git a/tests/components/sensor/test_canary.py b/tests/components/sensor/test_canary.py index 79e2bf4ee35..346929a4685 100644 --- a/tests/components/sensor/test_canary.py +++ b/tests/components/sensor/test_canary.py @@ -40,9 +40,9 @@ class TestCanarySensorSetup(unittest.TestCase): def test_setup_sensors(self): """Test the sensor setup.""" - online_device_at_home = mock_device(20, "Dining Room", True) - offline_device_at_home = mock_device(21, "Front Yard", False) - online_device_at_work = mock_device(22, "Office", True) + online_device_at_home = mock_device(20, "Dining Room", True, "Canary") + offline_device_at_home = mock_device(21, "Front Yard", False, "Canary") + online_device_at_work = mock_device(22, "Office", True, "Canary") self.hass.data[DATA_CANARY] = Mock() self.hass.data[DATA_CANARY].locations = [ @@ -57,7 +57,7 @@ class TestCanarySensorSetup(unittest.TestCase): def test_temperature_sensor(self): """Test temperature sensor with fahrenheit.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home", False) data = Mock() @@ -69,10 +69,11 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual("Home Family Room Temperature", sensor.name) self.assertEqual("°C", sensor.unit_of_measurement) self.assertEqual(21.12, sensor.state) + self.assertEqual("mdi:thermometer", sensor.icon) def test_temperature_sensor_with_none_sensor_value(self): """Test temperature sensor with fahrenheit.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home", False) data = Mock() @@ -85,7 +86,7 @@ class TestCanarySensorSetup(unittest.TestCase): def test_humidity_sensor(self): """Test humidity sensor.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home") data = Mock() @@ -97,10 +98,11 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual("Home Family Room Humidity", sensor.name) self.assertEqual("%", sensor.unit_of_measurement) self.assertEqual(50.46, sensor.state) + self.assertEqual("mdi:water-percent", sensor.icon) def test_air_quality_sensor_with_very_abnormal_reading(self): """Test air quality sensor.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home") data = Mock() @@ -112,13 +114,14 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual("Home Family Room Air Quality", sensor.name) self.assertEqual(None, sensor.unit_of_measurement) self.assertEqual(0.4, sensor.state) + self.assertEqual("mdi:weather-windy", sensor.icon) air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] self.assertEqual(STATE_AIR_QUALITY_VERY_ABNORMAL, air_quality) def test_air_quality_sensor_with_abnormal_reading(self): """Test air quality sensor.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home") data = Mock() @@ -130,13 +133,14 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual("Home Family Room Air Quality", sensor.name) self.assertEqual(None, sensor.unit_of_measurement) self.assertEqual(0.59, sensor.state) + self.assertEqual("mdi:weather-windy", sensor.icon) air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] self.assertEqual(STATE_AIR_QUALITY_ABNORMAL, air_quality) def test_air_quality_sensor_with_normal_reading(self): """Test air quality sensor.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home") data = Mock() @@ -148,13 +152,14 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual("Home Family Room Air Quality", sensor.name) self.assertEqual(None, sensor.unit_of_measurement) self.assertEqual(1.0, sensor.state) + self.assertEqual("mdi:weather-windy", sensor.icon) air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] self.assertEqual(STATE_AIR_QUALITY_NORMAL, air_quality) def test_air_quality_sensor_with_none_sensor_value(self): """Test air quality sensor.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home") data = Mock() @@ -165,3 +170,35 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual(None, sensor.state) self.assertEqual(None, sensor.device_state_attributes) + + def test_battery_sensor(self): + """Test battery sensor.""" + device = mock_device(10, "Family Room", "Canary Flex") + location = mock_location("Home") + + data = Mock() + data.get_reading.return_value = 70.4567 + + sensor = CanarySensor(data, SENSOR_TYPES[4], location, device) + sensor.update() + + self.assertEqual("Home Family Room Battery", sensor.name) + self.assertEqual("%", sensor.unit_of_measurement) + self.assertEqual(70.46, sensor.state) + self.assertEqual("mdi:battery-70", sensor.icon) + + def test_wifi_sensor(self): + """Test battery sensor.""" + device = mock_device(10, "Family Room", "Canary Flex") + location = mock_location("Home") + + data = Mock() + data.get_reading.return_value = -57 + + sensor = CanarySensor(data, SENSOR_TYPES[3], location, device) + sensor.update() + + self.assertEqual("Home Family Room Wifi", sensor.name) + self.assertEqual("dBm", sensor.unit_of_measurement) + self.assertEqual(-57, sensor.state) + self.assertEqual("mdi:wifi", sensor.icon) diff --git a/tests/components/test_canary.py b/tests/components/test_canary.py index 2c496c26e11..310f3be9f05 100644 --- a/tests/components/test_canary.py +++ b/tests/components/test_canary.py @@ -8,12 +8,16 @@ from tests.common import ( get_test_home_assistant) -def mock_device(device_id, name, is_online=True): +def mock_device(device_id, name, is_online=True, device_type_name=None): """Mock Canary Device class.""" device = MagicMock() type(device).device_id = PropertyMock(return_value=device_id) type(device).name = PropertyMock(return_value=name) type(device).is_online = PropertyMock(return_value=is_online) + type(device).device_type = PropertyMock(return_value={ + "id": 1, + "name": device_type_name, + }) return device From bf5894568045a2934d7e7584e6ce61ac7a4afbe6 Mon Sep 17 00:00:00 2001 From: PhracturedBlue Date: Fri, 30 Mar 2018 15:48:31 -0700 Subject: [PATCH 071/136] Fixes #12758. Try other cameras even if one fails to initialize (#13276) --- homeassistant/components/amcrest.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/amcrest.py b/homeassistant/components/amcrest.py index b91f1fae565..90331a3014e 100644 --- a/homeassistant/components/amcrest.py +++ b/homeassistant/components/amcrest.py @@ -10,6 +10,7 @@ from datetime import timedelta import aiohttp import voluptuous as vol from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectionError as ConnectError from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, @@ -93,14 +94,15 @@ def setup(hass, config): amcrest_cams = config[DOMAIN] for device in amcrest_cams: - camera = AmcrestCamera(device.get(CONF_HOST), - device.get(CONF_PORT), - device.get(CONF_USERNAME), - device.get(CONF_PASSWORD)).camera try: + camera = AmcrestCamera(device.get(CONF_HOST), + device.get(CONF_PORT), + device.get(CONF_USERNAME), + device.get(CONF_PASSWORD)).camera + # pylint: disable=pointless-statement camera.current_time - except (ConnectTimeout, HTTPError) as ex: + except (ConnectError, ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex)) hass.components.persistent_notification.create( 'Error: {}
' @@ -108,7 +110,7 @@ def setup(hass, config): ''.format(ex), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) - return False + continue ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS) name = device.get(CONF_NAME) From bf44dc422ceb546cc2f29b2c50b4277f480b4cd1 Mon Sep 17 00:00:00 2001 From: Tod Schmidt Date: Fri, 30 Mar 2018 20:22:48 -0400 Subject: [PATCH 072/136] Added HassOpenCover and HassCloseCover intents (#13372) * Added intents to cover * Added test for cover intents * Style fixes * Reverted reversions * Async fixes * Woof * Added conditional loading * Added conditional loading * Added conditional loading * Moved tests, fixed logic * Moved tests, fixed logic * Pylint * Pylint * Refactored componenet registration * Refactored componenet registration * Lint --- homeassistant/components/conversation.py | 32 ++++- homeassistant/components/cover/__init__.py | 10 ++ tests/components/cover/test_init.py | 49 ++++++++ tests/components/test_conversation.py | 130 ++++++++++++--------- tests/components/test_init.py | 47 ++++---- 5 files changed, 185 insertions(+), 83 deletions(-) create mode 100755 tests/components/cover/test_init.py diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index e96694ce0a3..ddd96c99177 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -13,10 +13,14 @@ from homeassistant import core from homeassistant.components import http from homeassistant.components.http.data_validator import ( RequestDataValidator) +from homeassistant.components.cover import (INTENT_OPEN_COVER, + INTENT_CLOSE_COVER) +from homeassistant.const import EVENT_COMPONENT_LOADED +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers import intent - from homeassistant.loader import bind_hass +from homeassistant.setup import (ATTR_COMPONENT) _LOGGER = logging.getLogger(__name__) @@ -28,6 +32,13 @@ DOMAIN = 'conversation' REGEX_TURN_COMMAND = re.compile(r'turn (?P(?: |\w)+) (?P\w+)') REGEX_TYPE = type(re.compile('')) +UTTERANCES = { + 'cover': { + INTENT_OPEN_COVER: ['Open [the] [a] [an] {name}[s]'], + INTENT_CLOSE_COVER: ['Close [the] [a] [an] {name}[s]'] + } +} + SERVICE_PROCESS = 'process' SERVICE_PROCESS_SCHEMA = vol.Schema({ @@ -112,6 +123,25 @@ async def async_setup(hass, config): '[the] [a] [an] {name}[s] toggle', ]) + @callback + def register_utterances(component): + """Register utterances for a component.""" + if component not in UTTERANCES: + return + for intent_type, sentences in UTTERANCES[component].items(): + async_register(hass, intent_type, sentences) + + @callback + def component_loaded(event): + """Handle a new component loaded.""" + register_utterances(event.data[ATTR_COMPONENT]) + + hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) + + # Check already loaded components. + for component in hass.config.components: + register_utterances(component) + return True diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index b24361d8293..e4c8f5634cf 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -17,6 +17,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.components import group +from homeassistant.helpers import intent from homeassistant.const import ( SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT, @@ -55,6 +56,9 @@ ATTR_CURRENT_TILT_POSITION = 'current_tilt_position' ATTR_POSITION = 'position' ATTR_TILT_POSITION = 'tilt_position' +INTENT_OPEN_COVER = 'HassOpenCover' +INTENT_CLOSE_COVER = 'HassCloseCover' + COVER_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) @@ -181,6 +185,12 @@ async def async_setup(hass, config): hass.services.async_register( DOMAIN, service_name, async_handle_cover_service, schema=schema) + hass.helpers.intent.async_register(intent.ServiceIntentHandler( + INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, + "Opened {}")) + hass.helpers.intent.async_register(intent.ServiceIntentHandler( + INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, + "Closed {}")) return True diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py new file mode 100755 index 00000000000..5df492d3d47 --- /dev/null +++ b/tests/components/cover/test_init.py @@ -0,0 +1,49 @@ +"""The tests for the cover platform.""" + +from homeassistant.components.cover import (SERVICE_OPEN_COVER, + SERVICE_CLOSE_COVER) +from homeassistant.components import intent +import homeassistant.components as comps +from tests.common import async_mock_service + + +async def test_open_cover_intent(hass): + """Test HassOpenCover intent.""" + result = await comps.cover.async_setup(hass, {}) + assert result + + hass.states.async_set('cover.garage_door', 'closed') + calls = async_mock_service(hass, 'cover', SERVICE_OPEN_COVER) + + response = await intent.async_handle( + hass, 'test', 'HassOpenCover', {'name': {'value': 'garage door'}} + ) + await hass.async_block_till_done() + + assert response.speech['plain']['speech'] == 'Opened garage door' + assert len(calls) == 1 + call = calls[0] + assert call.domain == 'cover' + assert call.service == 'open_cover' + assert call.data == {'entity_id': 'cover.garage_door'} + + +async def test_close_cover_intent(hass): + """Test HassCloseCover intent.""" + result = await comps.cover.async_setup(hass, {}) + assert result + + hass.states.async_set('cover.garage_door', 'open') + calls = async_mock_service(hass, 'cover', SERVICE_CLOSE_COVER) + + response = await intent.async_handle( + hass, 'test', 'HassCloseCover', {'name': {'value': 'garage door'}} + ) + await hass.async_block_till_done() + + assert response.speech['plain']['speech'] == 'Closed garage door' + assert len(calls) == 1 + call = calls[0] + assert call.domain == 'cover' + assert call.service == 'close_cover' + assert call.data == {'entity_id': 'cover.garage_door'} diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index bde00e10928..d9c29cdae83 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -1,26 +1,24 @@ """The tests for the Conversation component.""" # pylint: disable=protected-access -import asyncio - import pytest from homeassistant.setup import async_setup_component from homeassistant.components import conversation import homeassistant.components as component +from homeassistant.components.cover import (SERVICE_OPEN_COVER) from homeassistant.helpers import intent from tests.common import async_mock_intent, async_mock_service -@asyncio.coroutine -def test_calling_intent(hass): +async def test_calling_intent(hass): """Test calling an intent from a conversation.""" intents = async_mock_intent(hass, 'OrderBeer') - result = yield from component.async_setup(hass, {}) + result = await component.async_setup(hass, {}) assert result - result = yield from async_setup_component(hass, 'conversation', { + result = await async_setup_component(hass, 'conversation', { 'conversation': { 'intents': { 'OrderBeer': [ @@ -31,11 +29,11 @@ def test_calling_intent(hass): }) assert result - yield from hass.services.async_call( + await hass.services.async_call( 'conversation', 'process', { conversation.ATTR_TEXT: 'I would like the Grolsch beer' }) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(intents) == 1 intent = intents[0] @@ -45,8 +43,7 @@ def test_calling_intent(hass): assert intent.text_input == 'I would like the Grolsch beer' -@asyncio.coroutine -def test_register_before_setup(hass): +async def test_register_before_setup(hass): """Test calling an intent from a conversation.""" intents = async_mock_intent(hass, 'OrderBeer') @@ -54,7 +51,7 @@ def test_register_before_setup(hass): 'A {type} beer, please' ]) - result = yield from async_setup_component(hass, 'conversation', { + result = await async_setup_component(hass, 'conversation', { 'conversation': { 'intents': { 'OrderBeer': [ @@ -65,11 +62,11 @@ def test_register_before_setup(hass): }) assert result - yield from hass.services.async_call( + await hass.services.async_call( 'conversation', 'process', { conversation.ATTR_TEXT: 'A Grolsch beer, please' }) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(intents) == 1 intent = intents[0] @@ -78,11 +75,11 @@ def test_register_before_setup(hass): assert intent.slots == {'type': {'value': 'Grolsch'}} assert intent.text_input == 'A Grolsch beer, please' - yield from hass.services.async_call( + await hass.services.async_call( 'conversation', 'process', { conversation.ATTR_TEXT: 'I would like the Grolsch beer' }) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(intents) == 2 intent = intents[1] @@ -92,14 +89,14 @@ def test_register_before_setup(hass): assert intent.text_input == 'I would like the Grolsch beer' -@asyncio.coroutine -def test_http_processing_intent(hass, aiohttp_client): +async def test_http_processing_intent(hass, test_client): """Test processing intent via HTTP API.""" class TestIntentHandler(intent.IntentHandler): + """Test Intent Handler.""" + intent_type = 'OrderBeer' - @asyncio.coroutine - def async_handle(self, intent): + async def async_handle(self, intent): """Handle the intent.""" response = intent.create_response() response.async_set_speech( @@ -111,7 +108,7 @@ def test_http_processing_intent(hass, aiohttp_client): intent.async_register(hass, TestIntentHandler()) - result = yield from async_setup_component(hass, 'conversation', { + result = await async_setup_component(hass, 'conversation', { 'conversation': { 'intents': { 'OrderBeer': [ @@ -122,13 +119,13 @@ def test_http_processing_intent(hass, aiohttp_client): }) assert result - client = yield from aiohttp_client(hass.http.app) - resp = yield from client.post('/api/conversation/process', json={ + client = await test_client(hass.http.app) + resp = await client.post('/api/conversation/process', json={ 'text': 'I would like the Grolsch beer' }) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert data == { 'card': { @@ -145,24 +142,23 @@ def test_http_processing_intent(hass, aiohttp_client): } -@asyncio.coroutine @pytest.mark.parametrize('sentence', ('turn on kitchen', 'turn kitchen on')) -def test_turn_on_intent(hass, sentence): +async def test_turn_on_intent(hass, sentence): """Test calling the turn on intent.""" - result = yield from component.async_setup(hass, {}) + result = await component.async_setup(hass, {}) assert result - result = yield from async_setup_component(hass, 'conversation', {}) + result = await async_setup_component(hass, 'conversation', {}) assert result hass.states.async_set('light.kitchen', 'off') calls = async_mock_service(hass, 'homeassistant', 'turn_on') - yield from hass.services.async_call( + await hass.services.async_call( 'conversation', 'process', { conversation.ATTR_TEXT: sentence }) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] @@ -171,24 +167,49 @@ def test_turn_on_intent(hass, sentence): assert call.data == {'entity_id': 'light.kitchen'} -@asyncio.coroutine -@pytest.mark.parametrize('sentence', ('turn off kitchen', 'turn kitchen off')) -def test_turn_off_intent(hass, sentence): - """Test calling the turn on intent.""" - result = yield from component.async_setup(hass, {}) +async def test_cover_intents_loading(hass): + """Test Cover Intents Loading.""" + with pytest.raises(intent.UnknownIntent): + await intent.async_handle( + hass, 'test', 'HassOpenCover', {'name': {'value': 'garage door'}} + ) + + result = await async_setup_component(hass, 'cover', {}) assert result - result = yield from async_setup_component(hass, 'conversation', {}) + hass.states.async_set('cover.garage_door', 'closed') + calls = async_mock_service(hass, 'cover', SERVICE_OPEN_COVER) + + response = await intent.async_handle( + hass, 'test', 'HassOpenCover', {'name': {'value': 'garage door'}} + ) + await hass.async_block_till_done() + + assert response.speech['plain']['speech'] == 'Opened garage door' + assert len(calls) == 1 + call = calls[0] + assert call.domain == 'cover' + assert call.service == 'open_cover' + assert call.data == {'entity_id': 'cover.garage_door'} + + +@pytest.mark.parametrize('sentence', ('turn off kitchen', 'turn kitchen off')) +async def test_turn_off_intent(hass, sentence): + """Test calling the turn on intent.""" + result = await component.async_setup(hass, {}) + assert result + + result = await async_setup_component(hass, 'conversation', {}) assert result hass.states.async_set('light.kitchen', 'on') calls = async_mock_service(hass, 'homeassistant', 'turn_off') - yield from hass.services.async_call( + await hass.services.async_call( 'conversation', 'process', { conversation.ATTR_TEXT: sentence }) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] @@ -197,24 +218,23 @@ def test_turn_off_intent(hass, sentence): assert call.data == {'entity_id': 'light.kitchen'} -@asyncio.coroutine @pytest.mark.parametrize('sentence', ('toggle kitchen', 'kitchen toggle')) -def test_toggle_intent(hass, sentence): +async def test_toggle_intent(hass, sentence): """Test calling the turn on intent.""" - result = yield from component.async_setup(hass, {}) + result = await component.async_setup(hass, {}) assert result - result = yield from async_setup_component(hass, 'conversation', {}) + result = await async_setup_component(hass, 'conversation', {}) assert result hass.states.async_set('light.kitchen', 'on') calls = async_mock_service(hass, 'homeassistant', 'toggle') - yield from hass.services.async_call( + await hass.services.async_call( 'conversation', 'process', { conversation.ATTR_TEXT: sentence }) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] @@ -223,20 +243,19 @@ def test_toggle_intent(hass, sentence): assert call.data == {'entity_id': 'light.kitchen'} -@asyncio.coroutine -def test_http_api(hass, aiohttp_client): +async def test_http_api(hass, test_client): """Test the HTTP conversation API.""" - result = yield from component.async_setup(hass, {}) + result = await component.async_setup(hass, {}) assert result - result = yield from async_setup_component(hass, 'conversation', {}) + result = await async_setup_component(hass, 'conversation', {}) assert result - client = yield from aiohttp_client(hass.http.app) + client = await test_client(hass.http.app) hass.states.async_set('light.kitchen', 'off') calls = async_mock_service(hass, 'homeassistant', 'turn_on') - resp = yield from client.post('/api/conversation/process', json={ + resp = await client.post('/api/conversation/process', json={ 'text': 'Turn the kitchen on' }) assert resp.status == 200 @@ -248,23 +267,22 @@ def test_http_api(hass, aiohttp_client): assert call.data == {'entity_id': 'light.kitchen'} -@asyncio.coroutine -def test_http_api_wrong_data(hass, aiohttp_client): +async def test_http_api_wrong_data(hass, test_client): """Test the HTTP conversation API.""" - result = yield from component.async_setup(hass, {}) + result = await component.async_setup(hass, {}) assert result - result = yield from async_setup_component(hass, 'conversation', {}) + result = await async_setup_component(hass, 'conversation', {}) assert result - client = yield from aiohttp_client(hass.http.app) + client = await test_client(hass.http.app) - resp = yield from client.post('/api/conversation/process', json={ + resp = await client.post('/api/conversation/process', json={ 'text': 123 }) assert resp.status == 400 - resp = yield from client.post('/api/conversation/process', json={ + resp = await client.post('/api/conversation/process', json={ }) assert resp.status == 400 diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 991982af9b2..c8c7e0d809b 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -1,6 +1,5 @@ """The tests for Core components.""" # pylint: disable=protected-access -import asyncio import unittest from unittest.mock import patch, Mock @@ -75,9 +74,9 @@ class TestComponentsCore(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(calls)) - @asyncio.coroutine @patch('homeassistant.core.ServiceRegistry.call') - def test_turn_on_to_not_block_for_domains_without_service(self, mock_call): + async def test_turn_on_to_not_block_for_domains_without_service(self, + mock_call): """Test if turn_on is blocking domain with no service.""" async_mock_service(self.hass, 'light', SERVICE_TURN_ON) @@ -88,7 +87,7 @@ class TestComponentsCore(unittest.TestCase): 'entity_id': ['light.test', 'sensor.bla', 'light.bla'] }) service = self.hass.services._services['homeassistant']['turn_on'] - yield from service.func(service_call) + await service.func(service_call) self.assertEqual(2, mock_call.call_count) self.assertEqual( @@ -130,8 +129,8 @@ class TestComponentsCore(unittest.TestCase): comps.reload_core_config(self.hass) self.hass.block_till_done() - assert 10 == self.hass.config.latitude - assert 20 == self.hass.config.longitude + assert self.hass.config.latitude == 10 + assert self.hass.config.longitude == 20 ent.schedule_update_ha_state() self.hass.block_till_done() @@ -198,19 +197,18 @@ class TestComponentsCore(unittest.TestCase): assert not mock_stop.called -@asyncio.coroutine -def test_turn_on_intent(hass): +async def test_turn_on_intent(hass): """Test HassTurnOn intent.""" - result = yield from comps.async_setup(hass, {}) + result = await comps.async_setup(hass, {}) assert result hass.states.async_set('light.test_light', 'off') calls = async_mock_service(hass, 'light', SERVICE_TURN_ON) - response = yield from intent.async_handle( + response = await intent.async_handle( hass, 'test', 'HassTurnOn', {'name': {'value': 'test light'}} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert response.speech['plain']['speech'] == 'Turned test light on' assert len(calls) == 1 @@ -220,19 +218,18 @@ def test_turn_on_intent(hass): assert call.data == {'entity_id': ['light.test_light']} -@asyncio.coroutine -def test_turn_off_intent(hass): +async def test_turn_off_intent(hass): """Test HassTurnOff intent.""" - result = yield from comps.async_setup(hass, {}) + result = await comps.async_setup(hass, {}) assert result hass.states.async_set('light.test_light', 'on') calls = async_mock_service(hass, 'light', SERVICE_TURN_OFF) - response = yield from intent.async_handle( + response = await intent.async_handle( hass, 'test', 'HassTurnOff', {'name': {'value': 'test light'}} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert response.speech['plain']['speech'] == 'Turned test light off' assert len(calls) == 1 @@ -242,19 +239,18 @@ def test_turn_off_intent(hass): assert call.data == {'entity_id': ['light.test_light']} -@asyncio.coroutine -def test_toggle_intent(hass): +async def test_toggle_intent(hass): """Test HassToggle intent.""" - result = yield from comps.async_setup(hass, {}) + result = await comps.async_setup(hass, {}) assert result hass.states.async_set('light.test_light', 'off') calls = async_mock_service(hass, 'light', SERVICE_TOGGLE) - response = yield from intent.async_handle( + response = await intent.async_handle( hass, 'test', 'HassToggle', {'name': {'value': 'test light'}} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert response.speech['plain']['speech'] == 'Toggled test light' assert len(calls) == 1 @@ -264,13 +260,12 @@ def test_toggle_intent(hass): assert call.data == {'entity_id': ['light.test_light']} -@asyncio.coroutine -def test_turn_on_multiple_intent(hass): +async def test_turn_on_multiple_intent(hass): """Test HassTurnOn intent with multiple similar entities. This tests that matching finds the proper entity among similar names. """ - result = yield from comps.async_setup(hass, {}) + result = await comps.async_setup(hass, {}) assert result hass.states.async_set('light.test_light', 'off') @@ -278,10 +273,10 @@ def test_turn_on_multiple_intent(hass): hass.states.async_set('light.test_lighter', 'off') calls = async_mock_service(hass, 'light', SERVICE_TURN_ON) - response = yield from intent.async_handle( + response = await intent.async_handle( hass, 'test', 'HassTurnOn', {'name': {'value': 'test lights'}} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert response.speech['plain']['speech'] == 'Turned test lights 2 on' assert len(calls) == 1 From 72fb64695ed75e0eb675b15e97b34d823cc8b13b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 31 Mar 2018 15:07:29 +0200 Subject: [PATCH 073/136] Fix mysensors sensor type lookup (#13574) * Always return a safe default. --- homeassistant/components/sensor/mysensors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 3876b260dfc..66c36a8d9b1 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -81,5 +81,6 @@ class MySensorsSensor(mysensors.MySensorsEntity): TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT) sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None]) if isinstance(sensor_type, dict): - sensor_type = sensor_type.get(pres(self.child_type).name) + sensor_type = sensor_type.get( + pres(self.child_type).name, [None, None]) return sensor_type From 273a43be02715380cff7e73528137b0ea0ed410c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 31 Mar 2018 15:08:04 +0200 Subject: [PATCH 074/136] rfxtrx lib 0.22.0 (#13576) --- homeassistant/components/rfxtrx.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index e7301836d7e..d6873a0bd91 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -20,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyRFXtrx==0.21.1'] +REQUIREMENTS = ['pyRFXtrx==0.22.0'] DOMAIN = 'rfxtrx' diff --git a/requirements_all.txt b/requirements_all.txt index ccdb5fc5669..17ce0349ed2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ pyCEC==0.4.13 pyHS100==0.3.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.21.1 +pyRFXtrx==0.22.0 # homeassistant.components.sensor.tibber pyTibber==0.4.0 From 25185875344f55226653c8ed5bbbf8c737b5a19b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 31 Mar 2018 15:08:35 +0200 Subject: [PATCH 075/136] xiaomi lib upgrade (#13577) --- homeassistant/components/xiaomi_aqara.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 244605a7b97..48c54cdecff 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -23,7 +23,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.8.3'] +REQUIREMENTS = ['PyXiaomiGateway==0.9.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 17ce0349ed2..113f9bfbceb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.8.3 +PyXiaomiGateway==0.9.0 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From 3b4faa74a02a6bdd4a9cf283722ccca0db2f6c7c Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Sat, 31 Mar 2018 15:10:56 +0200 Subject: [PATCH 076/136] Remove MercedesME component (#13538) --- .coveragerc | 5 +- .../components/binary_sensor/mercedesme.py | 97 ----------- .../components/device_tracker/mercedesme.py | 74 --------- homeassistant/components/mercedesme.py | 156 ------------------ homeassistant/components/sensor/mercedesme.py | 87 ---------- requirements_all.txt | 3 - 6 files changed, 1 insertion(+), 421 deletions(-) delete mode 100644 homeassistant/components/binary_sensor/mercedesme.py delete mode 100644 homeassistant/components/device_tracker/mercedesme.py delete mode 100644 homeassistant/components/mercedesme.py delete mode 100644 homeassistant/components/sensor/mercedesme.py diff --git a/.coveragerc b/.coveragerc index 65f29767673..72bfb1269f5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -159,10 +159,7 @@ omit = homeassistant/components/maxcube.py homeassistant/components/*/maxcube.py - - homeassistant/components/mercedesme.py - homeassistant/components/*/mercedesme.py - + homeassistant/components/mochad.py homeassistant/components/*/mochad.py diff --git a/homeassistant/components/binary_sensor/mercedesme.py b/homeassistant/components/binary_sensor/mercedesme.py deleted file mode 100644 index fcf2d7122e2..00000000000 --- a/homeassistant/components/binary_sensor/mercedesme.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Support for Mercedes cars with Mercedes ME. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.mercedesme/ -""" -import logging -import datetime - -from homeassistant.components.binary_sensor import (BinarySensorDevice) -from homeassistant.components.mercedesme import ( - DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, BINARY_SENSORS) - -DEPENDENCIES = ['mercedesme'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the sensor platform.""" - data = hass.data[DATA_MME].data - - if not data.cars: - _LOGGER.error("No cars found. Check component log.") - return - - devices = [] - for car in data.cars: - for key, value in sorted(BINARY_SENSORS.items()): - if car['availabilities'].get(key, 'INVALID') == 'VALID': - devices.append(MercedesMEBinarySensor( - data, key, value[0], car["vin"], None)) - else: - _LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"]) - - add_devices(devices, True) - - -class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice): - """Representation of a Sensor.""" - - @property - def is_on(self): - """Return the state of the binary sensor.""" - return self._state - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._internal_name == "windowsClosed": - return { - "window_front_left": self._car["windowStatusFrontLeft"], - "window_front_right": self._car["windowStatusFrontRight"], - "window_rear_left": self._car["windowStatusRearLeft"], - "window_rear_right": self._car["windowStatusRearRight"], - "original_value": self._car[self._internal_name], - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"] - } - elif self._internal_name == "tireWarningLight": - return { - "front_right_tire_pressure_kpa": - self._car["frontRightTirePressureKpa"], - "front_left_tire_pressure_kpa": - self._car["frontLeftTirePressureKpa"], - "rear_right_tire_pressure_kpa": - self._car["rearRightTirePressureKpa"], - "rear_left_tire_pressure_kpa": - self._car["rearLeftTirePressureKpa"], - "original_value": self._car[self._internal_name], - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"] - ).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"], - } - return { - "original_value": self._car[self._internal_name], - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"] - } - - def update(self): - """Fetch new state data for the sensor.""" - self._car = next( - car for car in self._data.cars if car["vin"] == self._vin) - - if self._internal_name == "windowsClosed": - self._state = bool(self._car[self._internal_name] == "CLOSED") - elif self._internal_name == "tireWarningLight": - self._state = bool(self._car[self._internal_name] != "INACTIVE") - else: - self._state = self._car[self._internal_name] is True - - _LOGGER.debug("Updated %s Value: %s IsOn: %s", - self._internal_name, self._state, self.is_on) diff --git a/homeassistant/components/device_tracker/mercedesme.py b/homeassistant/components/device_tracker/mercedesme.py deleted file mode 100644 index dcc9e3ab2ec..00000000000 --- a/homeassistant/components/device_tracker/mercedesme.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Support for Mercedes cars with Mercedes ME. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/device_tracker.mercedesme/ -""" -import logging -from datetime import timedelta - -from homeassistant.components.mercedesme import DATA_MME -from homeassistant.helpers.event import track_time_interval -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['mercedesme'] - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) - - -def setup_scanner(hass, config, see, discovery_info=None): - """Set up the Mercedes ME tracker.""" - if discovery_info is None: - return False - - data = hass.data[DATA_MME].data - - if not data.cars: - return False - - MercedesMEDeviceTracker(hass, config, see, data) - - return True - - -class MercedesMEDeviceTracker(object): - """A class representing a Mercedes ME device tracker.""" - - def __init__(self, hass, config, see, data): - """Initialize the Mercedes ME device tracker.""" - self.see = see - self.data = data - self.update_info() - - track_time_interval( - hass, self.update_info, MIN_TIME_BETWEEN_SCANS) - - @Throttle(MIN_TIME_BETWEEN_SCANS) - def update_info(self, now=None): - """Update the device info.""" - for device in self.data.cars: - if not device['services'].get('VEHICLE_FINDER', False): - continue - - location = self.data.get_location(device["vin"]) - if location is None: - continue - - dev_id = device["vin"] - name = device["license"] - - lat = location['positionLat']['value'] - lon = location['positionLong']['value'] - attrs = { - 'trackr_id': dev_id, - 'id': dev_id, - 'name': name - } - self.see( - dev_id=dev_id, host_name=name, - gps=(lat, lon), attributes=attrs - ) - - return True diff --git a/homeassistant/components/mercedesme.py b/homeassistant/components/mercedesme.py deleted file mode 100644 index b809e46ec64..00000000000 --- a/homeassistant/components/mercedesme.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -Support for MercedesME System. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mercedesme/ -""" -import asyncio -import logging -from datetime import timedelta - -import voluptuous as vol -import homeassistant.helpers.config_validation as cv - -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, LENGTH_KILOMETERS) -from homeassistant.helpers import discovery -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_time_interval - -REQUIREMENTS = ['mercedesmejsonpy==0.1.2'] - -_LOGGER = logging.getLogger(__name__) - -BINARY_SENSORS = { - 'doorsClosed': ['Doors closed'], - 'windowsClosed': ['Windows closed'], - 'locked': ['Doors locked'], - 'tireWarningLight': ['Tire Warning'] -} - -SENSORS = { - 'fuelLevelPercent': ['Fuel Level', '%'], - 'fuelRangeKm': ['Fuel Range', LENGTH_KILOMETERS], - 'latestTrip': ['Latest Trip', None], - 'odometerKm': ['Odometer', LENGTH_KILOMETERS], - 'serviceIntervalDays': ['Next Service', 'days'] -} - -DATA_MME = 'mercedesme' -DOMAIN = 'mercedesme' - -FEATURE_NOT_AVAILABLE = "The feature %s is not available for your car %s" - -NOTIFICATION_ID = 'mercedesme_integration_notification' -NOTIFICATION_TITLE = 'Mercedes me integration setup' - -SIGNAL_UPDATE_MERCEDESME = "mercedesme_update" - - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=30): - vol.All(cv.positive_int, vol.Clamp(min=10)) - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up MercedesMe System.""" - from mercedesmejsonpy.controller import Controller - from mercedesmejsonpy import Exceptions - - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - scan_interval = conf.get(CONF_SCAN_INTERVAL) - - try: - mercedesme_api = Controller(username, password, scan_interval) - if not mercedesme_api.is_valid_session: - raise Exceptions.MercedesMeException(500) - hass.data[DATA_MME] = MercedesMeHub(mercedesme_api) - except Exceptions.MercedesMeException as ex: - if ex.code == 401: - hass.components.persistent_notification.create( - "Error:
Please check username and password." - "You will need to restart Home Assistant after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - else: - hass.components.persistent_notification.create( - "Error:
Can't communicate with Mercedes me API.
" - "Error code: {} Reason: {}" - "You will need to restart Home Assistant after fixing." - "".format(ex.code, ex.message), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - - _LOGGER.error("Unable to communicate with Mercedes me API: %s", - ex.message) - return False - - discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) - discovery.load_platform(hass, 'device_tracker', DOMAIN, {}, config) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) - - def hub_refresh(event_time): - """Call Mercedes me API to refresh information.""" - _LOGGER.info("Updating Mercedes me component.") - hass.data[DATA_MME].data.update() - dispatcher_send(hass, SIGNAL_UPDATE_MERCEDESME) - - track_time_interval( - hass, - hub_refresh, - timedelta(seconds=scan_interval)) - - return True - - -class MercedesMeHub(object): - """Representation of a base MercedesMe device.""" - - def __init__(self, data): - """Initialize the entity.""" - self.data = data - - -class MercedesMeEntity(Entity): - """Entity class for MercedesMe devices.""" - - def __init__(self, data, internal_name, sensor_name, vin, unit): - """Initialize the MercedesMe entity.""" - self._car = None - self._data = data - self._state = False - self._name = sensor_name - self._internal_name = internal_name - self._unit = unit - self._vin = vin - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @asyncio.coroutine - def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_MERCEDESME, self._update_callback) - - def _update_callback(self): - """Callback update method.""" - # If the method is made a callback this should be changed - # to the async version. Check core.callback - self.schedule_update_ha_state(True) - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit diff --git a/homeassistant/components/sensor/mercedesme.py b/homeassistant/components/sensor/mercedesme.py deleted file mode 100644 index bb7212678a7..00000000000 --- a/homeassistant/components/sensor/mercedesme.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Support for Mercedes cars with Mercedes ME. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sensor.mercedesme/ -""" -import logging -import datetime - -from homeassistant.components.mercedesme import ( - DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, SENSORS) - - -DEPENDENCIES = ['mercedesme'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the sensor platform.""" - if discovery_info is None: - return - - data = hass.data[DATA_MME].data - - if not data.cars: - return - - devices = [] - for car in data.cars: - for key, value in sorted(SENSORS.items()): - if car['availabilities'].get(key, 'INVALID') == 'VALID': - devices.append( - MercedesMESensor( - data, key, value[0], car["vin"], value[1])) - else: - _LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"]) - - add_devices(devices, True) - - -class MercedesMESensor(MercedesMeEntity): - """Representation of a Sensor.""" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): - """Get the latest data and updates the states.""" - _LOGGER.debug("Updating %s", self._internal_name) - - self._car = next( - car for car in self._data.cars if car["vin"] == self._vin) - - if self._internal_name == "latestTrip": - self._state = self._car["latestTrip"]["id"] - else: - self._state = self._car[self._internal_name] - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._internal_name == "latestTrip": - return { - "duration_seconds": - self._car["latestTrip"]["durationSeconds"], - "distance_traveled_km": - self._car["latestTrip"]["distanceTraveledKm"], - "started_at": datetime.datetime.fromtimestamp( - self._car["latestTrip"]["startedAt"] - ).strftime('%Y-%m-%d %H:%M:%S'), - "average_speed_km_per_hr": - self._car["latestTrip"]["averageSpeedKmPerHr"], - "finished": self._car["latestTrip"]["finished"], - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"] - ).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"] - } - - return { - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"] - } diff --git a/requirements_all.txt b/requirements_all.txt index 113f9bfbceb..761cb7d1eed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -495,9 +495,6 @@ matrix-client==0.0.6 # homeassistant.components.maxcube maxcube-api==0.1.0 -# homeassistant.components.mercedesme -mercedesmejsonpy==0.1.2 - # homeassistant.components.notify.message_bird messagebird==1.2.0 From 7bf8d4ab12fc3ec43aed18bb029225020d10ddab Mon Sep 17 00:00:00 2001 From: Myrddyn Date: Sat, 31 Mar 2018 17:01:07 -0400 Subject: [PATCH 077/136] Added Waze travel time sensor (#12387) * Added Waze travel time sensor * Update according PR comments and simplification --- .../components/sensor/waze_travel_time.py | 136 ++++++++++++++++++ requirements_all.txt | 3 + 2 files changed, 139 insertions(+) create mode 100644 homeassistant/components/sensor/waze_travel_time.py diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py new file mode 100644 index 00000000000..47589f33530 --- /dev/null +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -0,0 +1,136 @@ +""" +Support for Waze travel time sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.waze_travel_time/ +""" +from datetime import timedelta +import logging + +import requests +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_REGION +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['WazeRouteCalculator==0.5'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_DISTANCE = 'distance' +ATTR_ROUTE = 'route' + +CONF_ATTRIBUTION = "Data provided by the Waze.com" +CONF_DESTINATION = 'destination' +CONF_ORIGIN = 'origin' + +DEFAULT_NAME = 'Waze Travel Time' + +ICON = 'mdi:car' + +REGIONS = ['US', 'NA', 'EU', 'IL'] + +SCAN_INTERVAL = timedelta(minutes=5) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ORIGIN): cv.string, + vol.Required(CONF_DESTINATION): cv.string, + vol.Required(CONF_REGION): vol.In(REGIONS), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Waze travel time sensor platform.""" + destination = config.get(CONF_DESTINATION) + name = config.get(CONF_NAME) + origin = config.get(CONF_ORIGIN) + region = config.get(CONF_REGION) + + try: + waze_data = WazeRouteData(origin, destination, region) + except requests.exceptions.HTTPError as error: + _LOGGER.error("%s", error) + return + + add_devices([WazeTravelTime(waze_data, name)], True) + + +class WazeTravelTime(Entity): + """Representation of a Waze travel time sensor.""" + + def __init__(self, waze_data, name): + """Initialize the Waze travel time sensor.""" + self._name = name + self._state = None + self.waze_data = waze_data + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return round(self._state['duration']) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return 'min' + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON + + @property + def device_state_attributes(self): + """Return the state attributes of the last update.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_DISTANCE: round(self._state['distance']), + ATTR_ROUTE: self._state['route'], + } + + def update(self): + """Fetch new state data for the sensor.""" + try: + self.waze_data.update() + self._state = self.waze_data.data + except KeyError: + _LOGGER.error("Error retrieving data from server") + + +class WazeRouteData(object): + """Get data from Waze.""" + + def __init__(self, origin, destination, region): + """Initialize the data object.""" + self._destination = destination + self._origin = origin + self._region = region + self.data = {} + + @Throttle(SCAN_INTERVAL) + def update(self): + """Fetch latest data from Waze.""" + import WazeRouteCalculator + _LOGGER.debug("Update in progress...") + try: + params = WazeRouteCalculator.WazeRouteCalculator( + self._origin, self._destination, self._region, None) + results = params.calc_all_routes_info() + best_route = next(iter(results)) + (duration, distance) = results[best_route] + best_route_str = bytes(best_route, 'ISO-8859-1').decode('UTF-8') + self.data['duration'] = duration + self.data['distance'] = distance + self.data['route'] = best_route_str + except WazeRouteCalculator.WRCError as exp: + _LOGGER.error("Error on retrieving data: %s", exp) + return diff --git a/requirements_all.txt b/requirements_all.txt index 761cb7d1eed..7ddac50245c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,6 +54,9 @@ TravisPy==0.3.5 # homeassistant.components.notify.twitter TwitterAPI==2.5.0 +# homeassistant.components.sensor.waze_travel_time +WazeRouteCalculator==0.5 + # homeassistant.components.notify.yessssms YesssSMS==0.1.1b3 From 477f7ec01e49126c8fd85a90059940937a2507c6 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 31 Mar 2018 23:15:25 +0200 Subject: [PATCH 078/136] Added switch component to Amcrest IP Camera. (#12992) * Added switch component to Amcrest IP Camera. * Fixes to new switch component after review * Removed redundant branching, as well as requirement declaration. * Changes to requirements after rerunning generation script * Minor changes --- homeassistant/components/amcrest.py | 20 ++++- homeassistant/components/switch/amcrest.py | 92 ++++++++++++++++++++++ requirements_all.txt | 2 +- 3 files changed, 111 insertions(+), 3 deletions(-) create mode 100755 homeassistant/components/switch/amcrest.py diff --git a/homeassistant/components/amcrest.py b/homeassistant/components/amcrest.py index 90331a3014e..d0e470e3f8e 100644 --- a/homeassistant/components/amcrest.py +++ b/homeassistant/components/amcrest.py @@ -14,11 +14,11 @@ from requests.exceptions import ConnectionError as ConnectError from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, - CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION) + CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['amcrest==1.2.1'] +REQUIREMENTS = ['amcrest==1.2.2'] DEPENDENCIES = ['ffmpeg'] _LOGGER = logging.getLogger(__name__) @@ -64,6 +64,12 @@ SENSORS = { 'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'], } +# Switch types are defined like: Name, icon +SWITCHES = { + 'motion_detection': ['Motion Detection', 'mdi:run-fast'], + 'motion_recording': ['Motion Recording', 'mdi:record-rec'] +} + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ vol.Required(CONF_HOST): cv.string, @@ -82,6 +88,8 @@ CONFIG_SCHEMA = vol.Schema({ cv.time_period, vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSORS)]), + vol.Optional(CONF_SWITCHES): + vol.All(cv.ensure_list, [vol.In(SWITCHES)]), })]) }, extra=vol.ALLOW_EXTRA) @@ -116,6 +124,7 @@ def setup(hass, config): name = device.get(CONF_NAME) resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)] sensors = device.get(CONF_SENSORS) + switches = device.get(CONF_SWITCHES) stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)] username = device.get(CONF_USERNAME) @@ -145,6 +154,13 @@ def setup(hass, config): CONF_SENSORS: sensors, }, config) + if switches: + discovery.load_platform( + hass, 'switch', DOMAIN, { + CONF_NAME: name, + CONF_SWITCHES: switches + }, config) + return True diff --git a/homeassistant/components/switch/amcrest.py b/homeassistant/components/switch/amcrest.py new file mode 100755 index 00000000000..0b93bc98b10 --- /dev/null +++ b/homeassistant/components/switch/amcrest.py @@ -0,0 +1,92 @@ +""" +Support for toggling Amcrest IP camera settings. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.amcrest/ +""" +import asyncio +import logging + +from homeassistant.components.amcrest import DATA_AMCREST, SWITCHES +from homeassistant.const import ( + CONF_NAME, CONF_SWITCHES, STATE_OFF, STATE_ON) +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['amcrest'] + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the IP Amcrest camera switch platform.""" + if discovery_info is None: + return + + name = discovery_info[CONF_NAME] + switches = discovery_info[CONF_SWITCHES] + camera = hass.data[DATA_AMCREST][name].device + + all_switches = [] + + for setting in switches: + all_switches.append(AmcrestSwitch(setting, camera)) + + async_add_devices(all_switches, True) + + +class AmcrestSwitch(ToggleEntity): + """Representation of an Amcrest IP camera switch.""" + + def __init__(self, setting, camera): + """Initialize the Amcrest switch.""" + self._setting = setting + self._camera = camera + self._name = SWITCHES[setting][0] + self._icon = SWITCHES[setting][1] + self._state = None + + @property + def name(self): + """Return the name of the switch if any.""" + return self._name + + @property + def state(self): + """Return the state of the switch.""" + return self._state + + @property + def is_on(self): + """Return true if switch is on.""" + return self._state == STATE_ON + + def turn_on(self, **kwargs): + """Turn setting on.""" + if self._setting == 'motion_detection': + self._camera.motion_detection = 'true' + elif self._setting == 'motion_recording': + self._camera.motion_recording = 'true' + + def turn_off(self, **kwargs): + """Turn setting off.""" + if self._setting == 'motion_detection': + self._camera.motion_detection = 'false' + elif self._setting == 'motion_recording': + self._camera.motion_recording = 'false' + + def update(self): + """Update setting state.""" + _LOGGER.debug("Polling state for setting: %s ", self._name) + + if self._setting == 'motion_detection': + detection = self._camera.is_motion_detector_on() + elif self._setting == 'motion_recording': + detection = self._camera.is_record_on_motion_detection() + + self._state = STATE_ON if detection else STATE_OFF + + @property + def icon(self): + """Return the icon for the switch.""" + return self._icon diff --git a/requirements_all.txt b/requirements_all.txt index 7ddac50245c..77b57e23769 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -98,7 +98,7 @@ alarmdecoder==1.13.2 alpha_vantage==1.9.0 # homeassistant.components.amcrest -amcrest==1.2.1 +amcrest==1.2.2 # homeassistant.components.media_player.anthemav anthemav==1.1.8 From 12affa1469df6b404e3b4c64e425ae720fcd782a Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Sat, 31 Mar 2018 17:16:47 -0400 Subject: [PATCH 079/136] Upgrade pyhydroquebec 2.2.1 (#13586) --- homeassistant/components/sensor/hydroquebec.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/hydroquebec.py b/homeassistant/components/sensor/hydroquebec.py index 3678ac9268f..9129ee17d80 100644 --- a/homeassistant/components/sensor/hydroquebec.py +++ b/homeassistant/components/sensor/hydroquebec.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyhydroquebec==2.1.0'] +REQUIREMENTS = ['pyhydroquebec==2.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 77b57e23769..94271e5e23e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -768,7 +768,7 @@ pyhiveapi==0.2.11 pyhomematic==0.1.40 # homeassistant.components.sensor.hydroquebec -pyhydroquebec==2.1.0 +pyhydroquebec==2.2.1 # homeassistant.components.alarm_control_panel.ialarm pyialarm==0.2 From 7b3d17bae41b224101cb0a5e61e0827957ee1234 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 31 Mar 2018 23:20:58 +0200 Subject: [PATCH 080/136] Add mastodon (#13441) * Add mastodon * Move login * Revert "Move login" This reverts commit 2c8446f62950f91c0ebfc0b4825e87421ac653fc. --- .coveragerc | 3 +- homeassistant/components/notify/mastodon.py | 70 +++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/notify/mastodon.py diff --git a/.coveragerc b/.coveragerc index 72bfb1269f5..828da909a06 100644 --- a/.coveragerc +++ b/.coveragerc @@ -507,6 +507,7 @@ omit = homeassistant/components/notify/kodi.py homeassistant/components/notify/lannouncer.py homeassistant/components/notify/llamalab_automate.py + homeassistant/components/notify/mastodon.py homeassistant/components/notify/matrix.py homeassistant/components/notify/message_bird.py homeassistant/components/notify/mycroft.py @@ -522,8 +523,8 @@ omit = homeassistant/components/notify/sendgrid.py homeassistant/components/notify/simplepush.py homeassistant/components/notify/slack.py - homeassistant/components/notify/stride.py homeassistant/components/notify/smtp.py + homeassistant/components/notify/stride.py homeassistant/components/notify/synology_chat.py homeassistant/components/notify/syslog.py homeassistant/components/notify/telegram.py diff --git a/homeassistant/components/notify/mastodon.py b/homeassistant/components/notify/mastodon.py new file mode 100644 index 00000000000..3ba95407fec --- /dev/null +++ b/homeassistant/components/notify/mastodon.py @@ -0,0 +1,70 @@ +""" +Mastodon platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.mastodon/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.notify import ( + PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_ACCESS_TOKEN +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['Mastodon.py==1.2.2'] + +_LOGGER = logging.getLogger(__name__) + +CONF_BASE_URL = 'base_url' +CONF_CLIENT_ID = 'client_id' +CONF_CLIENT_SECRET = 'client_secret' + +DEFAULT_URL = 'https://mastodon.social' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_BASE_URL, default=DEFAULT_URL): cv.string, +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Mastodon notification service.""" + from mastodon import Mastodon + from mastodon.Mastodon import MastodonUnauthorizedError + + client_id = config.get(CONF_CLIENT_ID) + client_secret = config.get(CONF_CLIENT_SECRET) + access_token = config.get(CONF_ACCESS_TOKEN) + base_url = config.get(CONF_BASE_URL) + + try: + mastodon = Mastodon( + client_id=client_id, client_secret=client_secret, + access_token=access_token, api_base_url=base_url) + mastodon.account_verify_credentials() + except MastodonUnauthorizedError: + _LOGGER.warning("Authentication failed") + return None + + return MastodonNotificationService(mastodon) + + +class MastodonNotificationService(BaseNotificationService): + """Implement the notification service for Mastodon.""" + + def __init__(self, api): + """Initialize the service.""" + self._api = api + + def send_message(self, message="", **kwargs): + """Send a message to a user.""" + from mastodon.Mastodon import MastodonAPIError + + try: + self._api.toot(message) + except MastodonAPIError: + _LOGGER.error("Unable to send message") diff --git a/requirements_all.txt b/requirements_all.txt index 94271e5e23e..7eeed2d1ef2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -24,6 +24,9 @@ DoorBirdPy==0.1.3 # homeassistant.components.homekit HAP-python==1.1.7 +# homeassistant.components.notify.mastodon +Mastodon.py==1.2.2 + # homeassistant.components.isy994 PyISY==1.1.0 From 7c99567b65a5124c202d37f85a71b475282a303b Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Sat, 31 Mar 2018 23:22:54 +0200 Subject: [PATCH 081/136] Added support for requesting RSSI values from Bluetooth devices (#12458) * Added support for requesting RSSI values from Bluetooth devices * Moved Bluetooth RSSI code to separate library and imported it * Cleaned up tuple issues * Changed concatination of mac addresses * Changed string formatting to use new style * Ran gen_requirements_all.py --- .../device_tracker/bluetooth_tracker.py | 32 +++++++++++++------ requirements_all.txt | 3 ++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py index 9d41611d9a2..807f6c0d0a4 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -17,12 +17,15 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pybluez==0.22'] +REQUIREMENTS = ['pybluez==0.22', 'bt_proximity==0.1.2'] BT_PREFIX = 'BT_' +CONF_REQUEST_RSSI = 'request_rssi' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_TRACK_NEW): cv.boolean + vol.Optional(CONF_TRACK_NEW): cv.boolean, + vol.Optional(CONF_REQUEST_RSSI): cv.boolean }) @@ -30,11 +33,15 @@ def setup_scanner(hass, config, see, discovery_info=None): """Set up the Bluetooth Scanner.""" # pylint: disable=import-error import bluetooth + from bt_proximity import BluetoothRSSI - def see_device(device): + def see_device(mac, name, rssi=None): """Mark a device as seen.""" - see(mac=BT_PREFIX + device[0], host_name=device[1], - source_type=SOURCE_TYPE_BLUETOOTH) + attributes = {} + if rssi is not None: + attributes['rssi'] = rssi + see(mac="{}_{}".format(BT_PREFIX, mac), host_name=name, + attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH) def discover_devices(): """Discover Bluetooth devices.""" @@ -64,27 +71,32 @@ def setup_scanner(hass, config, see, discovery_info=None): if track_new: for dev in discover_devices(): if dev[0] not in devs_to_track and \ - dev[0] not in devs_donot_track: + dev[0] not in devs_donot_track: devs_to_track.append(dev[0]) - see_device(dev) + see_device(dev[0], dev[1]) interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + request_rssi = config.get(CONF_REQUEST_RSSI, False) + def update_bluetooth(now): """Lookup Bluetooth device and update status.""" try: if track_new: for dev in discover_devices(): if dev[0] not in devs_to_track and \ - dev[0] not in devs_donot_track: + dev[0] not in devs_donot_track: devs_to_track.append(dev[0]) for mac in devs_to_track: _LOGGER.debug("Scanning %s", mac) result = bluetooth.lookup_name(mac, timeout=5) - if not result: + rssi = None + if request_rssi: + rssi = BluetoothRSSI(mac).request_rssi() + if result is None: # Could not lookup device name continue - see_device((mac, result)) + see_device(mac, result, rssi) except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") track_point_in_utc_time( diff --git a/requirements_all.txt b/requirements_all.txt index 7eeed2d1ef2..b2098b37f63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,6 +172,9 @@ boto3==1.4.7 # homeassistant.scripts.credstash botocore==1.7.34 +# homeassistant.components.device_tracker.bluetooth_tracker +bt_proximity==0.1.2 + # homeassistant.components.sensor.buienradar # homeassistant.components.weather.buienradar buienradar==0.91 From 5fce2e2b47da639f28aa61db0ddfa7521d1bd742 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 1 Apr 2018 02:45:50 +0200 Subject: [PATCH 082/136] Fix mysensors update callback (#13602) * Add callback annotation to mysensors dispatch callback. --- homeassistant/components/mysensors.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index a560b49648f..74df860a0fc 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -19,6 +19,7 @@ from homeassistant.components.mqtt import ( from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) +from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -301,9 +302,9 @@ def setup(hass, config): """Call MQTT publish function.""" mqtt.publish(hass, topic, payload, qos, retain) - def sub_callback(topic, callback, qos): + def sub_callback(topic, sub_cb, qos): """Call MQTT subscribe function.""" - mqtt.subscribe(hass, topic, callback, qos) + mqtt.subscribe(hass, topic, sub_cb, qos) gateway = mysensors.MQTTGateway( pub_callback, sub_callback, event_callback=None, persistence=persistence, @@ -627,6 +628,7 @@ class MySensorsEntity(MySensorsDevice, Entity): """Return true if entity is available.""" return self.value_type in self._values + @callback def _async_update_callback(self): """Update the entity.""" self.async_schedule_update_ha_state(True) From 45ef34ff81ed5f63a56253a0ddced8db4849b028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 1 Apr 2018 10:09:16 +0200 Subject: [PATCH 083/136] Broadlink (#13585) * Update broadlink lib * Update broadlink lib * requirements --- homeassistant/components/sensor/broadlink.py | 6 ++---- homeassistant/components/switch/broadlink.py | 12 +++++------- requirements_all.txt | 8 ++++---- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index 47cefe50aec..044b77ebfe8 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -19,9 +19,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = [ - 'https://github.com/balloob/python-broadlink/archive/' - '3580ff2eaccd267846f14246d6ede6e30671f7c6.zip#broadlink==0.5.1'] +REQUIREMENTS = ['broadlink==0.8.0'] _LOGGER = logging.getLogger(__name__) @@ -108,7 +106,7 @@ class BroadlinkData(object): """Initialize the data object.""" import broadlink self.data = None - self._device = broadlink.a1((ip_addr, 80), mac_addr) + self._device = broadlink.a1((ip_addr, 80), mac_addr, None) self._device.timeout = timeout self._schema = vol.Schema({ vol.Optional('temperature'): vol.Range(min=-50, max=150), diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 38888733ba6..3828758fe6e 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -22,9 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from homeassistant.util.dt import utcnow -REQUIREMENTS = [ - 'https://github.com/balloob/python-broadlink/archive/' - '3580ff2eaccd267846f14246d6ede6e30671f7c6.zip#broadlink==0.5.1'] +REQUIREMENTS = ['broadlink==0.8.0'] _LOGGER = logging.getLogger(__name__) @@ -142,7 +140,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return slots['slot_{}'.format(slot)] if switch_type in RM_TYPES: - broadlink_device = broadlink.rm((ip_addr, 80), mac_addr) + broadlink_device = broadlink.rm((ip_addr, 80), mac_addr, None) hass.services.register(DOMAIN, SERVICE_LEARN + '_' + ip_addr.replace('.', '_'), _learn_command) hass.services.register(DOMAIN, SERVICE_SEND + '_' + @@ -159,14 +157,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ) ) elif switch_type in SP1_TYPES: - broadlink_device = broadlink.sp1((ip_addr, 80), mac_addr) + broadlink_device = broadlink.sp1((ip_addr, 80), mac_addr, None) switches = [BroadlinkSP1Switch(friendly_name, broadlink_device)] elif switch_type in SP2_TYPES: - broadlink_device = broadlink.sp2((ip_addr, 80), mac_addr) + broadlink_device = broadlink.sp2((ip_addr, 80), mac_addr, None) switches = [BroadlinkSP2Switch(friendly_name, broadlink_device)] elif switch_type in MP1_TYPES: switches = [] - broadlink_device = broadlink.mp1((ip_addr, 80), mac_addr) + broadlink_device = broadlink.mp1((ip_addr, 80), mac_addr, None) parent_device = BroadlinkMP1Switch(broadlink_device) for i in range(1, 5): slot = BroadlinkMP1Slot( diff --git a/requirements_all.txt b/requirements_all.txt index 4a21044e6ab..2b7967f4981 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,6 +172,10 @@ boto3==1.4.7 # homeassistant.scripts.credstash botocore==1.7.34 +# homeassistant.components.sensor.broadlink +# homeassistant.components.switch.broadlink +broadlink==0.8.0 + # homeassistant.components.device_tracker.bluetooth_tracker bt_proximity==0.1.2 @@ -392,10 +396,6 @@ httplib2==0.10.3 # homeassistant.components.media_player.braviatv https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7 -# homeassistant.components.sensor.broadlink -# homeassistant.components.switch.broadlink -https://github.com/balloob/python-broadlink/archive/3580ff2eaccd267846f14246d6ede6e30671f7c6.zip#broadlink==0.5.1 - # homeassistant.components.media_player.spotify https://github.com/happyleavesaoc/spotipy/archive/544614f4b1d508201d363e84e871f86c90aa26b2.zip#spotipy==2.4.4 From a8fdd76f44aa29482ab522e4d5f02c474a0f69a8 Mon Sep 17 00:00:00 2001 From: Zhao Date: Sun, 1 Apr 2018 20:17:26 +1000 Subject: [PATCH 084/136] Fix IMAP email message_data (#13606) --- homeassistant/components/sensor/imap_email_content.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sensor/imap_email_content.py b/homeassistant/components/sensor/imap_email_content.py index 1f04cd606d6..c0c9bf62efd 100644 --- a/homeassistant/components/sensor/imap_email_content.py +++ b/homeassistant/components/sensor/imap_email_content.py @@ -87,6 +87,8 @@ class EmailReader(object): _, message_data = self.connection.uid( 'fetch', message_uid, '(RFC822)') + if message_data is None: + return None raw_email = message_data[0][1] email_message = email.message_from_bytes(raw_email) return email_message From 0c0e0c36af141cd2ed4a720783953a49b66e4657 Mon Sep 17 00:00:00 2001 From: Lewis Juggins <873275+lwis@users.noreply.github.com> Date: Sun, 1 Apr 2018 15:50:48 +0100 Subject: [PATCH 085/136] Re-add group polling as a fallback for observation (#13613) --- homeassistant/components/light/tradfri.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 227ed419aec..63468225c9d 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -76,11 +76,6 @@ class TradfriGroup(Light): """Return unique ID for this group.""" return self._unique_id - @property - def should_poll(self): - """No polling needed for tradfri group.""" - return False - @property def supported_features(self): """Flag supported features.""" @@ -149,6 +144,10 @@ class TradfriGroup(Light): self._refresh(tradfri_device) self.async_schedule_update_ha_state() + async def async_update(self): + """Fetch new state data for the group.""" + await self._group.update() + class TradfriLight(Light): """The platform class required by Home Assistant.""" From ff9f500c515163e5f775b5fba02847081e397024 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Apr 2018 08:30:14 -0700 Subject: [PATCH 086/136] Unflake folder watcher test (#13569) * Unflake folder watcher test * Fix tests * Lint --- requirements_test_all.txt | 3 - script/gen_requirements_all.py | 1 - tests/components/test_folder_watcher.py | 96 ++++++++++++------------- 3 files changed, 44 insertions(+), 56 deletions(-) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 990848d5c56..d3e9ae51a60 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -202,8 +202,5 @@ wakeonlan==1.0.0 # homeassistant.components.cloud warrant==0.6.1 -# homeassistant.components.folder_watcher -watchdog==0.8.3 - # homeassistant.components.sensor.yahoo_finance yahoo-finance==1.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index fa39c307f18..d5bb2701e9b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -91,7 +91,6 @@ TEST_REQUIREMENTS = ( 'yahoo-finance', 'pythonwhois', 'wakeonlan', - 'watchdog', 'vultr' ) diff --git a/tests/components/test_folder_watcher.py b/tests/components/test_folder_watcher.py index 587d8b7ad6d..16ec7a58a02 100644 --- a/tests/components/test_folder_watcher.py +++ b/tests/components/test_folder_watcher.py @@ -1,64 +1,56 @@ """The tests for the folder_watcher component.""" -import unittest -from unittest.mock import MagicMock +from unittest.mock import Mock, patch import os from homeassistant.components import folder_watcher -from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant - -CWD = os.path.join(os.path.dirname(__file__)) -FILE = 'file.txt' +from homeassistant.setup import async_setup_component +from tests.common import MockDependency -class TestFolderWatcher(unittest.TestCase): - """Test the file_watcher component.""" +async def test_invalid_path_setup(hass): + """Test that a invalid path is not setup.""" + assert not await async_setup_component( + hass, folder_watcher.DOMAIN, { + folder_watcher.DOMAIN: { + folder_watcher.CONF_FOLDER: 'invalid_path' + } + }) - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.whitelist_external_dirs = set((CWD)) - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() +async def test_valid_path_setup(hass): + """Test that a valid path is setup.""" + cwd = os.path.join(os.path.dirname(__file__)) + hass.config.whitelist_external_dirs = set((cwd)) + with patch.object(folder_watcher, 'Watcher'): + assert await async_setup_component( + hass, folder_watcher.DOMAIN, { + folder_watcher.DOMAIN: {folder_watcher.CONF_FOLDER: cwd} + }) - def test_invalid_path_setup(self): - """Test that a invalid path is not setup.""" - config = { - folder_watcher.DOMAIN: [{ - folder_watcher.CONF_FOLDER: 'invalid_path' - }] - } - self.assertFalse( - setup_component(self.hass, folder_watcher.DOMAIN, config)) - def test_valid_path_setup(self): - """Test that a valid path is setup.""" - config = { - folder_watcher.DOMAIN: [{folder_watcher.CONF_FOLDER: CWD}] - } +@MockDependency('watchdog', 'events') +def test_event(mock_watchdog): + """Check that HASS events are fired correctly on watchdog event.""" + class MockPatternMatchingEventHandler: + """Mock base class for the pattern matcher event handler.""" - self.assertTrue(setup_component( - self.hass, folder_watcher.DOMAIN, config)) + def __init__(self, patterns): + pass - def test_event(self): - """Check that HASS events are fired correctly on watchdog event.""" - from watchdog.events import FileModifiedEvent - - # Cant use setup_component as need to retrieve Watcher object. - w = folder_watcher.Watcher(CWD, - folder_watcher.DEFAULT_PATTERN, - self.hass) - w.startup(None) - - self.hass.bus.fire = MagicMock() - - # Trigger a fake filesystem event through the Watcher Observer emitter. - (emitter,) = w._observer.emitters - emitter.queue_event(FileModifiedEvent(FILE)) - - # Wait for the event to propagate. - self.hass.block_till_done() - - assert self.hass.bus.fire.called + mock_watchdog.events.PatternMatchingEventHandler = \ + MockPatternMatchingEventHandler + hass = Mock() + handler = folder_watcher.create_event_handler(['*'], hass) + handler.on_created(Mock( + is_directory=False, + src_path='/hello/world.txt', + event_type='created' + )) + assert hass.bus.fire.called + assert hass.bus.fire.mock_calls[0][1][0] == folder_watcher.DOMAIN + assert hass.bus.fire.mock_calls[0][1][1] == { + 'event_type': 'created', + 'path': '/hello/world.txt', + 'file': 'world.txt', + 'folder': '/hello', + } From c8f2810fac989d2a8275bfb92603a5877113beed Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 1 Apr 2018 17:36:26 +0200 Subject: [PATCH 087/136] Make mysensors updates and platform setup async (#13603) * Use async updates but keep methods that interact with mysensors gateway thread, eg turn_on and turn_off, non async. * Use Python 3.5 async syntax. --- .../components/binary_sensor/mysensors.py | 7 +++-- homeassistant/components/climate/mysensors.py | 12 ++++--- homeassistant/components/cover/mysensors.py | 8 +++-- .../components/device_tracker/mysensors.py | 20 ++++++------ homeassistant/components/light/mysensors.py | 31 ++++++++++--------- homeassistant/components/mysensors.py | 19 ++++++------ homeassistant/components/notify/mysensors.py | 4 +-- homeassistant/components/sensor/mysensors.py | 6 ++-- homeassistant/components/switch/mysensors.py | 15 ++++----- 9 files changed, 65 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 1e9359b6902..21443021193 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -21,11 +21,12 @@ SENSORS = { } -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors platform for binary sensors.""" +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the mysensors platform for binary sensors.""" mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, MySensorsBinarySensor, - add_devices=add_devices) + async_add_devices=async_add_devices) class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice): diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index b526d8b066c..2545094ceec 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -31,10 +31,12 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_OPERATION_MODE) -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors climate.""" +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the mysensors climate.""" mysensors.setup_mysensors_platform( - hass, DOMAIN, discovery_info, MySensorsHVAC, add_devices=add_devices) + hass, DOMAIN, discovery_info, MySensorsHVAC, + async_add_devices=async_add_devices) class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): @@ -163,8 +165,8 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): self._values[self.value_type] = operation_mode self.schedule_update_ha_state() - def update(self): + async def async_update(self): """Update the controller with the latest value from a sensor.""" - super().update() + await super().async_update() self._values[self.value_type] = DICT_MYS_TO_HA[ self._values[self.value_type]] diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index 391d2a22bda..669a7ce6723 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -9,10 +9,12 @@ from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice from homeassistant.const import STATE_OFF, STATE_ON -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors platform for covers.""" +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the mysensors platform for covers.""" mysensors.setup_mysensors_platform( - hass, DOMAIN, discovery_info, MySensorsCover, add_devices=add_devices) + hass, DOMAIN, discovery_info, MySensorsCover, + async_add_devices=async_add_devices) class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): diff --git a/homeassistant/components/device_tracker/mysensors.py b/homeassistant/components/device_tracker/mysensors.py index f68eb361ca0..b0d29bf0566 100644 --- a/homeassistant/components/device_tracker/mysensors.py +++ b/homeassistant/components/device_tracker/mysensors.py @@ -6,15 +6,15 @@ https://home-assistant.io/components/device_tracker.mysensors/ """ from homeassistant.components import mysensors from homeassistant.components.device_tracker import DOMAIN -from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify -def setup_scanner(hass, config, see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the MySensors device scanner.""" new_devices = mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, MySensorsDeviceScanner, - device_args=(see, )) + device_args=(async_see, )) if not new_devices: return False @@ -22,9 +22,9 @@ def setup_scanner(hass, config, see, discovery_info=None): dev_id = ( id(device.gateway), device.node_id, device.child_id, device.value_type) - dispatcher_connect( + async_dispatcher_connect( hass, mysensors.SIGNAL_CALLBACK.format(*dev_id), - device.update_callback) + device.async_update_callback) return True @@ -32,20 +32,20 @@ def setup_scanner(hass, config, see, discovery_info=None): class MySensorsDeviceScanner(mysensors.MySensorsDevice): """Represent a MySensors scanner.""" - def __init__(self, see, *args): + def __init__(self, async_see, *args): """Set up instance.""" super().__init__(*args) - self.see = see + self.async_see = async_see - def update_callback(self): + async def async_update_callback(self): """Update the device.""" - self.update() + await self.async_update() node = self.gateway.sensors[self.node_id] child = node.children[self.child_id] position = child.values[self.value_type] latitude, longitude, _ = position.split(',') - self.see( + await self.async_see( dev_id=slugify(self.name), host_name=self.name, gps=(latitude, longitude), diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 7aa1e754c43..6e41e0f5693 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -15,8 +15,9 @@ import homeassistant.util.color as color_util SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors platform for lights.""" +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the mysensors platform for lights.""" device_class_map = { 'S_DIMMER': MySensorsLightDimmer, 'S_RGB_LIGHT': MySensorsLightRGB, @@ -24,7 +25,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): } mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, device_class_map, - add_devices=add_devices) + async_add_devices=async_add_devices) class MySensorsLight(mysensors.MySensorsEntity, Light): @@ -140,12 +141,12 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): self._values[value_type] = STATE_OFF self.schedule_update_ha_state() - def _update_light(self): + def _async_update_light(self): """Update the controller with values from light child.""" value_type = self.gateway.const.SetReq.V_LIGHT self._state = self._values[value_type] == STATE_ON - def _update_dimmer(self): + def _async_update_dimmer(self): """Update the controller with values from dimmer child.""" value_type = self.gateway.const.SetReq.V_DIMMER if value_type in self._values: @@ -153,7 +154,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): if self._brightness == 0: self._state = False - def _update_rgb_or_w(self): + def _async_update_rgb_or_w(self): """Update the controller with values from RGB or RGBW child.""" value = self._values[self.value_type] color_list = rgb_hex_to_rgb_list(value) @@ -177,11 +178,11 @@ class MySensorsLightDimmer(MySensorsLight): if self.gateway.optimistic: self.schedule_update_ha_state() - def update(self): + async def async_update(self): """Update the controller with the latest value from a sensor.""" - super().update() - self._update_light() - self._update_dimmer() + await super().async_update() + self._async_update_light() + self._async_update_dimmer() class MySensorsLightRGB(MySensorsLight): @@ -203,12 +204,12 @@ class MySensorsLightRGB(MySensorsLight): if self.gateway.optimistic: self.schedule_update_ha_state() - def update(self): + async def async_update(self): """Update the controller with the latest value from a sensor.""" - super().update() - self._update_light() - self._update_dimmer() - self._update_rgb_or_w() + await super().async_update() + self._async_update_light() + self._async_update_dimmer() + self._async_update_rgb_or_w() class MySensorsLightRGBW(MySensorsLightRGB): diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 74df860a0fc..17c9129a31d 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -4,7 +4,6 @@ Connect to a MySensors gateway via pymysensors API. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mysensors/ """ -import asyncio from collections import defaultdict import logging import os @@ -519,11 +518,12 @@ def get_mysensors_gateway(hass, gateway_id): return gateways.get(gateway_id) +@callback def setup_mysensors_platform( hass, domain, discovery_info, device_class, device_args=None, - add_devices=None): + async_add_devices=None): """Set up a MySensors platform.""" - # Only act if called via MySensors by discovery event. + # Only act if called via mysensors by discovery event. # Otherwise gateway is not setup. if not discovery_info: return @@ -552,8 +552,8 @@ def setup_mysensors_platform( new_devices.append(devices[dev_id]) if new_devices: _LOGGER.info("Adding new devices: %s", new_devices) - if add_devices is not None: - add_devices(new_devices, True) + if async_add_devices is not None: + async_add_devices(new_devices, True) return new_devices @@ -596,7 +596,7 @@ class MySensorsDevice(object): return attr - def update(self): + async def async_update(self): """Update the controller with the latest value from a sensor.""" node = self.gateway.sensors[self.node_id] child = node.children[self.child_id] @@ -629,14 +629,13 @@ class MySensorsEntity(MySensorsDevice, Entity): return self.value_type in self._values @callback - def _async_update_callback(self): + def async_update_callback(self): """Update the entity.""" self.async_schedule_update_ha_state(True) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register update callback.""" dev_id = id(self.gateway), self.node_id, self.child_id, self.value_type async_dispatcher_connect( self.hass, SIGNAL_CALLBACK.format(*dev_id), - self._async_update_callback) + self.async_update_callback) diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py index 8ae697048f5..257b5995446 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -9,12 +9,12 @@ from homeassistant.components.notify import ( ATTR_TARGET, DOMAIN, BaseNotificationService) -def get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Get the MySensors notification service.""" new_devices = mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, MySensorsNotificationDevice) if not new_devices: - return + return None return MySensorsNotificationService(hass) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 66c36a8d9b1..669ef3998de 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -34,10 +34,12 @@ SENSORS = { } -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the MySensors platform for sensors.""" mysensors.setup_mysensors_platform( - hass, DOMAIN, discovery_info, MySensorsSensor, add_devices=add_devices) + hass, DOMAIN, discovery_info, MySensorsSensor, + async_add_devices=async_add_devices) class MySensorsSensor(mysensors.MySensorsEntity): diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index b4a1dcde3e6..c0f45cad861 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -20,7 +20,8 @@ SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the mysensors platform for switches.""" device_class_map = { 'S_DOOR': MySensorsSwitch, @@ -39,7 +40,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): } mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, device_class_map, - add_devices=add_devices) + async_add_devices=async_add_devices) def send_ir_code_service(service): """Set IR code as device state attribute.""" @@ -59,9 +60,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for device in _devices: device.turn_on(**kwargs) - hass.services.register(DOMAIN, SERVICE_SEND_IR_CODE, - send_ir_code_service, - schema=SEND_IR_CODE_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_SEND_IR_CODE, send_ir_code_service, + schema=SEND_IR_CODE_SERVICE_SCHEMA) class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice): @@ -143,7 +144,7 @@ class MySensorsIRSwitch(MySensorsSwitch): self._values[set_req.V_LIGHT] = STATE_OFF self.schedule_update_ha_state() - def update(self): + async def async_update(self): """Update the controller with the latest value from a sensor.""" - super().update() + await super().async_update() self._ir_code = self._values.get(self.value_type) From dee47d50ecf4f0e80a127d624027c1688d34b504 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Mon, 2 Apr 2018 01:37:03 +1000 Subject: [PATCH 088/136] Use 0/1 for raspberry pi cover GPIO writes rather than true/false (#13610) * Use 0/1 for GPIO writes rather than true/false GPIO pins don't appear to respond to true/false writes, and this is reflected in code elsewhere. For example, in `\components\switch\rpio_gpio.py` the following code is used: ``` def turn_on(self, **kwargs): """Turn the device on.""" rpi_gpio.write_output(self._port, 0 if self._invert_logic else 1) self._state = True self.schedule_update_ha_state() ``` This code works. Hence this PR uses 0/1 in the raspberry pi GPIO cover, instead of true/false. * Update rpi_gpio.py --- homeassistant/components/cover/rpi_gpio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py index 77cd0b0f7e2..49666139330 100644 --- a/homeassistant/components/cover/rpi_gpio.py +++ b/homeassistant/components/cover/rpi_gpio.py @@ -87,7 +87,7 @@ class RPiGPIOCover(CoverDevice): self._invert_relay = invert_relay rpi_gpio.setup_output(self._relay_pin) rpi_gpio.setup_input(self._state_pin, self._state_pull_mode) - rpi_gpio.write_output(self._relay_pin, not self._invert_relay) + rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1) @property def name(self): @@ -105,9 +105,9 @@ class RPiGPIOCover(CoverDevice): def _trigger(self): """Trigger the cover.""" - rpi_gpio.write_output(self._relay_pin, self._invert_relay) + rpi_gpio.write_output(self._relay_pin, 1 if self._invert_relay else 0) sleep(self._relay_time) - rpi_gpio.write_output(self._relay_pin, not self._invert_relay) + rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1) def close_cover(self, **kwargs): """Close the cover.""" From cd96d7b2ecf36beb065e19211578dee95265e5d1 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 1 Apr 2018 17:38:29 +0200 Subject: [PATCH 089/136] Add pincode fallback (#13587) * Add pincode log statement * Moved msg to show_setup_msg --- homeassistant/components/homekit/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 2fa2ebd396a..af2c74d9c3c 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -36,6 +36,7 @@ def validate_entity_config(values): def show_setup_message(bridge, hass): """Display persistent notification with setup information.""" pin = bridge.pincode.decode() + _LOGGER.info('Pincode: %s', pin) message = 'To setup Home Assistant in the Home App, enter the ' \ 'following code:\n### {}'.format(pin) hass.components.persistent_notification.create( From eb763764b39c8913c913c85ef81b783cef73577f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Apr 2018 09:03:01 -0700 Subject: [PATCH 090/136] Fix Hue error logging (#13616) --- homeassistant/components/hue/bridge.py | 7 ++++++- tests/components/light/test_hue.py | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 790831a4d6c..8093c84971e 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -31,9 +31,14 @@ class HueBridge(object): self.available = True self.api = None + @property + def host(self): + """Return the host of this bridge.""" + return self.config_entry.data['host'] + async def async_setup(self, tries=0): """Set up a phue bridge based on host parameter.""" - host = self.config_entry.data['host'] + host = self.host try: self.api = await get_bridge( diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index d73531b1b9a..7b6c3a21a79 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -160,7 +160,13 @@ LIGHT_RESPONSE = { @pytest.fixture def mock_bridge(hass): """Mock a Hue bridge.""" - bridge = Mock(available=True, allow_groups=False, host='1.1.1.1') + bridge = Mock( + available=True, + allow_unreachable=False, + allow_groups=False, + api=Mock(), + spec=hue.HueBridge + ) bridge.mock_requests = [] # We're using a deque so we can schedule multiple responses # and also means that `popleft()` will blow up if we get more updates From 4ad0152a44b0cdb8f121b9f6a9cf04dcbe8ac9a1 Mon Sep 17 00:00:00 2001 From: Lewis Juggins <873275+lwis@users.noreply.github.com> Date: Sun, 1 Apr 2018 18:42:47 +0100 Subject: [PATCH 091/136] Bugfix for tradfri to correctly execute Command. (#13618) --- homeassistant/components/light/tradfri.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 63468225c9d..95082bb4d19 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -146,7 +146,7 @@ class TradfriGroup(Light): async def async_update(self): """Fetch new state data for the group.""" - await self._group.update() + await self._api(self._group.update()) class TradfriLight(Light): From be43c3bcfeb771369c230006f638b2db5aa07b26 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Sun, 1 Apr 2018 14:12:55 -0400 Subject: [PATCH 092/136] Fix mqtt_json color commands (#13617) --- homeassistant/components/light/mqtt_json.py | 30 ++++++++++++--------- tests/components/light/test_mqtt_json.py | 27 +++++++++++++++++++ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 25212e45c60..20e49e40bae 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -129,6 +129,8 @@ class MqttJson(MqttAvailability, Light): self._retain = retain self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None self._state = False + self._rgb = rgb + self._xy = xy if brightness: self._brightness = 255 else: @@ -307,20 +309,22 @@ class MqttJson(MqttAvailability, Light): message = {'state': 'ON'} - if ATTR_HS_COLOR in kwargs: + if ATTR_HS_COLOR in kwargs and (self._rgb or self._xy): hs_color = kwargs[ATTR_HS_COLOR] - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) - rgb = color_util.color_hsv_to_RGB( - hs_color[0], hs_color[1], brightness / 255 * 100) - xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) - message['color'] = { - 'r': rgb[0], - 'g': rgb[1], - 'b': rgb[2], - 'x': xy_color[0], - 'y': xy_color[1], - } + message['color'] = {} + if self._rgb: + brightness = kwargs.get( + ATTR_BRIGHTNESS, + self._brightness if self._brightness else 255) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness / 255 * 100) + message['color']['r'] = rgb[0] + message['color']['g'] = rgb[1] + message['color']['b'] = rgb[2] + if self._xy: + xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + message['color']['x'] = xy_color[0] + message['color']['y'] = xy_color[1] if self._optimistic: self._hs = kwargs[ATTR_HS_COLOR] diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index cfeffc93108..a183355fbb3 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -334,6 +334,33 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual('colorloop', state.attributes['effect']) self.assertEqual(170, state.attributes['white_value']) + # Test a color command + light.turn_on(self.hass, 'light.test', + brightness=50, hs_color=(125, 100)) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.async_publish.mock_calls[0][1][0]) + self.assertEqual(2, + self.mock_publish.async_publish.mock_calls[0][1][2]) + self.assertEqual(False, + self.mock_publish.async_publish.mock_calls[0][1][3]) + # Get the sent message + message_json = json.loads( + self.mock_publish.async_publish.mock_calls[1][1][1]) + self.assertEqual(50, message_json["brightness"]) + self.assertEqual({ + 'r': 0, + 'g': 50, + 'b': 4, + }, message_json["color"]) + self.assertEqual("ON", message_json["state"]) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(50, state.attributes['brightness']) + self.assertEqual((125, 100), state.attributes['hs_color']) + def test_flash_short_and_long(self): \ # pylint: disable=invalid-name """Test for flash length being sent when included.""" From 9fb73c1bab113669a8d2b1cb326dd11bdfa7e727 Mon Sep 17 00:00:00 2001 From: Niklas Wagner Date: Mon, 2 Apr 2018 09:45:38 +0200 Subject: [PATCH 093/136] Hue mireds value is actually 153 not 154 (#13601) --- homeassistant/components/light/__init__.py | 4 +++- tests/components/google_assistant/test_smart_home.py | 2 +- tests/components/light/test_demo.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index eea6c821fc0..39d3203795e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -457,12 +457,14 @@ class Light(ToggleEntity): def min_mireds(self): """Return the coldest color_temp that this light supports.""" # Default to the Philips Hue value that HA has always assumed - return 154 + # https://developers.meethue.com/documentation/core-concepts + return 153 @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" # Default to the Philips Hue value that HA has always assumed + # https://developers.meethue.com/documentation/core-concepts return 500 @property diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index dd9373c782a..e284b026ad8 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -74,7 +74,7 @@ async def test_sync_message(hass): 'willReportState': False, 'attributes': { 'colorModel': 'rgb', - 'temperatureMinK': 6493, + 'temperatureMinK': 6535, 'temperatureMaxK': 2000, }, 'roomHint': 'Living Room' diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index ff984aff221..963cda6abc4 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -53,7 +53,7 @@ class TestDemoLight(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertEqual(400, state.attributes.get(light.ATTR_COLOR_TEMP)) - self.assertEqual(154, state.attributes.get(light.ATTR_MIN_MIREDS)) + self.assertEqual(153, state.attributes.get(light.ATTR_MIN_MIREDS)) self.assertEqual(500, state.attributes.get(light.ATTR_MAX_MIREDS)) self.assertEqual('none', state.attributes.get(light.ATTR_EFFECT)) light.turn_on(self.hass, ENTITY_LIGHT, kelvin=3000, brightness_pct=50) From 53f08e313fede1c3813fdfbb2765fd6a01786c6c Mon Sep 17 00:00:00 2001 From: Wolfgang Malgadey Date: Mon, 2 Apr 2018 10:36:38 +0200 Subject: [PATCH 094/136] changed PyTado version (#13626) --- homeassistant/components/tado.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tado.py b/homeassistant/components/tado.py index cfba0a5c0c4..7c045518132 100644 --- a/homeassistant/components/tado.py +++ b/homeassistant/components/tado.py @@ -15,7 +15,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.util import Throttle -REQUIREMENTS = ['python-tado==0.2.2'] +REQUIREMENTS = ['python-tado==0.2.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2b7967f4981..95cb8c5462a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1001,7 +1001,7 @@ python-songpal==0.0.7 python-synology==0.1.0 # homeassistant.components.tado -python-tado==0.2.2 +python-tado==0.2.3 # homeassistant.components.telegram_bot python-telegram-bot==10.0.1 From 95e98925d1cc864c243bccc9a02f3897f099f959 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 2 Apr 2018 11:58:22 +0200 Subject: [PATCH 095/136] Upgrade py-cpuinfo to 4.0.0 (#13629) --- homeassistant/components/sensor/cpuspeed.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/cpuspeed.py b/homeassistant/components/sensor/cpuspeed.py index 25b7bba506c..c39ae43aef0 100644 --- a/homeassistant/components/sensor/cpuspeed.py +++ b/homeassistant/components/sensor/cpuspeed.py @@ -13,7 +13,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['py-cpuinfo==3.3.0'] +REQUIREMENTS = ['py-cpuinfo==4.0.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 95cb8c5462a..c1a44c8535b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -648,7 +648,7 @@ py-august==0.4.0 py-canary==0.5.0 # homeassistant.components.sensor.cpuspeed -py-cpuinfo==3.3.0 +py-cpuinfo==4.0.0 # homeassistant.components.melissa py-melissa-climate==1.0.6 From b342c43b09c54c746973afeb50389b2be7d2baef Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 2 Apr 2018 14:02:06 +0200 Subject: [PATCH 096/136] Add Switzerland (#13630) * Add Switzerland * remove space --- .../components/binary_sensor/workday.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index f5a7324d351..8935ad5115d 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -30,8 +30,8 @@ ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES', - 'Sweden', 'SE', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', - 'Wales'] + 'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK', + 'UnitedStates', 'US', 'Wales'] CONF_COUNTRY = 'country' CONF_PROVINCE = 'province' CONF_WORKDAYS = 'workdays' @@ -47,13 +47,13 @@ DEFAULT_OFFSET = 0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES), - vol.Optional(CONF_PROVINCE): cv.string, + vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): + vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int), + vol.Optional(CONF_PROVINCE): cv.string, vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS): vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]), - vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): - vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]), }) @@ -74,14 +74,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if province: # 'state' and 'prov' are not interchangeable, so need to make # sure we use the right one - if (hasattr(obj_holidays, "PROVINCES") and + if (hasattr(obj_holidays, 'PROVINCES') and province in obj_holidays.PROVINCES): - obj_holidays = getattr(holidays, country)(prov=province, - years=year) - elif (hasattr(obj_holidays, "STATES") and + obj_holidays = getattr(holidays, country)( + prov=province, years=year) + elif (hasattr(obj_holidays, 'STATES') and province in obj_holidays.STATES): - obj_holidays = getattr(holidays, country)(state=province, - years=year) + obj_holidays = getattr(holidays, country)( + state=province, years=year) else: _LOGGER.error("There is no province/state %s in country %s", province, country) From 9ce4755f8ae18309dd28910ee7ff519fc90d46f1 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 2 Apr 2018 19:45:12 +0200 Subject: [PATCH 097/136] Xiaomi Mi WiFi Repeater 2 integration as device tracker (#13521) * Xiaomi Mi WiFi Repeater 2 integration as device tracker * Unused import removed * python-miio version pinned * Missing period added --- .../components/device_tracker/xiaomi_miio.py | 77 +++++++++++++++++++ requirements_all.txt | 1 + 2 files changed, 78 insertions(+) create mode 100644 homeassistant/components/device_tracker/xiaomi_miio.py diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/device_tracker/xiaomi_miio.py new file mode 100644 index 00000000000..61568892388 --- /dev/null +++ b/homeassistant/components/device_tracker/xiaomi_miio.py @@ -0,0 +1,77 @@ +""" +Support for Xiaomi Mi WiFi Repeater 2. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/device_tracker.xiaomi_miio/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA, + DeviceScanner) +from homeassistant.const import (CONF_HOST, CONF_TOKEN) + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), +}) + +REQUIREMENTS = ['python-miio==0.3.9'] + + +def get_scanner(hass, config): + """Return a Xiaomi MiIO device scanner.""" + from miio import WifiRepeater, DeviceException + + scanner = None + host = config[DOMAIN].get(CONF_HOST) + token = config[DOMAIN].get(CONF_TOKEN) + + _LOGGER.info( + "Initializing with host %s (token %s...)", host, token[:5]) + + try: + device = WifiRepeater(host, token) + device_info = device.info() + _LOGGER.info("%s %s %s detected", + device_info.model, + device_info.firmware_version, + device_info.hardware_version) + scanner = XiaomiMiioDeviceScanner(hass, device) + except DeviceException as ex: + _LOGGER.error("Device unavailable or token incorrect: %s", ex) + + return scanner + + +class XiaomiMiioDeviceScanner(DeviceScanner): + """This class queries a Xiaomi Mi WiFi Repeater.""" + + def __init__(self, hass, device): + """Initialize the scanner.""" + self.device = device + + async def async_scan_devices(self): + """Scan for devices and return a list containing found device ids.""" + from miio import DeviceException + + devices = [] + try: + station_info = await self.hass.async_add_job(self.device.status) + _LOGGER.debug("Got new station info: %s", station_info) + + for device in station_info['mat']: + devices.append(device['mac']) + + except DeviceException as ex: + _LOGGER.error("Got exception while fetching the state: %s", ex) + + return devices + + async def async_get_device_name(self, device): + """The repeater doesn't provide the name of the associated device.""" + return None diff --git a/requirements_all.txt b/requirements_all.txt index c1a44c8535b..e071049cfc8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -961,6 +961,7 @@ python-juicenet==0.0.5 # homeassistant.components.lirc # python-lirc==1.2.3 +# homeassistant.components.device_tracker.xiaomi_miio # homeassistant.components.fan.xiaomi_miio # homeassistant.components.light.xiaomi_miio # homeassistant.components.remote.xiaomi_miio From 89f5a938c7940fde3d1d96cecb988a4ca5d538bb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 3 Apr 2018 18:27:08 +0200 Subject: [PATCH 098/136] Upgrade youtube_dl to 2018.04.03 (#13647) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index e10a713995b..85c569789a2 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.03.10'] +REQUIREMENTS = ['youtube_dl==2018.04.03'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index e071049cfc8..a2e48d931c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1333,7 +1333,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.03.10 +youtube_dl==2018.04.03 # homeassistant.components.light.zengge zengge==0.2 From bfb49c2e58429e5c4860cc4787c92feacf3fcb80 Mon Sep 17 00:00:00 2001 From: Kevin Raddatz Date: Tue, 3 Apr 2018 18:28:42 +0200 Subject: [PATCH 099/136] Update plex.py (#13659) fixed IndexError on line 131 --- homeassistant/components/sensor/plex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py index 505983cb3a7..b61e1bce0da 100644 --- a/homeassistant/components/sensor/plex.py +++ b/homeassistant/components/sensor/plex.py @@ -128,7 +128,7 @@ class PlexSensor(Entity): season_title += " ({0})".format(sess.show().year) season_episode = "S{0}".format(sess.parentIndex) if sess.index is not None: - season_episode += " · E{1}".format(sess.index) + season_episode += " · E{0}".format(sess.index) episode_title = sess.title now_playing_title = "{0} - {1} - {2}".format(season_title, season_episode, From 92bd932679a9455826f36913dde88b5bd025a9f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Apr 2018 14:23:21 -0700 Subject: [PATCH 100/136] Always enable config entries & remove config_entry_example (#13663) --- homeassistant/components/config/__init__.py | 10 +- .../.translations/de.json | 24 ----- .../.translations/en.json | 24 ----- .../.translations/fi.json | 11 --- .../.translations/ko.json | 24 ----- .../.translations/nl.json | 24 ----- .../.translations/no.json | 24 ----- .../.translations/pl.json | 24 ----- .../.translations/ro.json | 15 --- .../.translations/sl.json | 24 ----- .../.translations/vi.json | 24 ----- .../.translations/zh-Hans.json | 24 ----- .../config_entry_example/__init__.py | 98 ------------------- .../config_entry_example/strings.json | 24 ----- homeassistant/config_entries.py | 3 +- tests/components/test_config_entry_example.py | 38 ------- 16 files changed, 2 insertions(+), 413 deletions(-) delete mode 100644 homeassistant/components/config_entry_example/.translations/de.json delete mode 100644 homeassistant/components/config_entry_example/.translations/en.json delete mode 100644 homeassistant/components/config_entry_example/.translations/fi.json delete mode 100644 homeassistant/components/config_entry_example/.translations/ko.json delete mode 100644 homeassistant/components/config_entry_example/.translations/nl.json delete mode 100644 homeassistant/components/config_entry_example/.translations/no.json delete mode 100644 homeassistant/components/config_entry_example/.translations/pl.json delete mode 100644 homeassistant/components/config_entry_example/.translations/ro.json delete mode 100644 homeassistant/components/config_entry_example/.translations/sl.json delete mode 100644 homeassistant/components/config_entry_example/.translations/vi.json delete mode 100644 homeassistant/components/config_entry_example/.translations/zh-Hans.json delete mode 100644 homeassistant/components/config_entry_example/__init__.py delete mode 100644 homeassistant/components/config_entry_example/strings.json delete mode 100644 tests/components/test_config_entry_example.py diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 601b12ffe4a..4d0295c382a 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -14,24 +14,16 @@ from homeassistant.util.yaml import load_yaml, dump DOMAIN = 'config' DEPENDENCIES = ['http'] SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script', - 'entity_registry') + 'entity_registry', 'config_entries') ON_DEMAND = ('zwave',) -FEATURE_FLAGS = ('config_entries',) @asyncio.coroutine def async_setup(hass, config): """Set up the config component.""" - global SECTIONS - yield from hass.components.frontend.async_register_built_in_panel( 'config', 'config', 'mdi:settings') - # Temporary way of allowing people to opt-in for unreleased config sections - for key, value in config.get(DOMAIN, {}).items(): - if key in FEATURE_FLAGS and value: - SECTIONS += (key,) - @asyncio.coroutine def setup_panel(panel_name): """Set up a panel.""" diff --git a/homeassistant/components/config_entry_example/.translations/de.json b/homeassistant/components/config_entry_example/.translations/de.json deleted file mode 100644 index 75b88f2f822..00000000000 --- a/homeassistant/components/config_entry_example/.translations/de.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "Ung\u00fcltige Objekt-ID" - }, - "step": { - "init": { - "data": { - "object_id": "Objekt-ID" - }, - "description": "Bitte gib eine Objekt_ID f\u00fcr das Test-Entity ein.", - "title": "W\u00e4hle eine Objekt-ID" - }, - "name": { - "data": { - "name": "Name" - }, - "description": "Bitte gib einen Namen f\u00fcr das Test-Entity ein", - "title": "Name des Test-Entity" - } - }, - "title": "Beispiel Konfig-Eintrag" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/en.json b/homeassistant/components/config_entry_example/.translations/en.json deleted file mode 100644 index ec24d01ebc8..00000000000 --- a/homeassistant/components/config_entry_example/.translations/en.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "Invalid object ID" - }, - "step": { - "init": { - "data": { - "object_id": "Object ID" - }, - "description": "Please enter an object_id for the test entity.", - "title": "Pick object id" - }, - "name": { - "data": { - "name": "Name" - }, - "description": "Please enter a name for the test entity.", - "title": "Name of the entity" - } - }, - "title": "Config Entry Example" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/fi.json b/homeassistant/components/config_entry_example/.translations/fi.json deleted file mode 100644 index 054a6f372bc..00000000000 --- a/homeassistant/components/config_entry_example/.translations/fi.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "name": { - "data": { - "name": "Nimi" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/ko.json b/homeassistant/components/config_entry_example/.translations/ko.json deleted file mode 100644 index f12e3fc52f1..00000000000 --- a/homeassistant/components/config_entry_example/.translations/ko.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "\uc624\ube0c\uc81d\ud2b8 ID\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "step": { - "init": { - "data": { - "object_id": "\uc624\ube0c\uc81d\ud2b8 ID" - }, - "description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc624\ube0c\uc81d\ud2b8 ID \ub97c \uc785\ub825\ud558\uc138\uc694", - "title": "\uc624\ube0c\uc81d\ud2b8 ID \uc120\ud0dd" - }, - "name": { - "data": { - "name": "\uc774\ub984" - }, - "description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984\uc744 \uc785\ub825\ud558\uc138\uc694.", - "title": "\uad6c\uc131\uc694\uc18c \uc774\ub984" - } - }, - "title": "\uc785\ub825 \uc608\uc81c \uad6c\uc131" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/nl.json b/homeassistant/components/config_entry_example/.translations/nl.json deleted file mode 100644 index 7b52ac88cf0..00000000000 --- a/homeassistant/components/config_entry_example/.translations/nl.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "Ongeldig object ID" - }, - "step": { - "init": { - "data": { - "object_id": "Object ID" - }, - "description": "Voer een object_id in voor het testen van de entiteit.", - "title": "Kies object id" - }, - "name": { - "data": { - "name": "Naam" - }, - "description": "Voer een naam in voor het testen van de entiteit.", - "title": "Naam van de entiteit" - } - }, - "title": "Voorbeeld van de config vermelding" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/no.json b/homeassistant/components/config_entry_example/.translations/no.json deleted file mode 100644 index 380c539f8af..00000000000 --- a/homeassistant/components/config_entry_example/.translations/no.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "Ugyldig objekt ID" - }, - "step": { - "init": { - "data": { - "object_id": "Objekt ID" - }, - "description": "Vennligst skriv inn en object_id for testenheten.", - "title": "Velg objekt ID" - }, - "name": { - "data": { - "name": "Navn" - }, - "description": "Vennligst skriv inn et navn for testenheten.", - "title": "Navn p\u00e5 enheten" - } - }, - "title": "Konfigureringseksempel" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/pl.json b/homeassistant/components/config_entry_example/.translations/pl.json deleted file mode 100644 index 35cca168249..00000000000 --- a/homeassistant/components/config_entry_example/.translations/pl.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "Nieprawid\u0142owy identyfikator obiektu" - }, - "step": { - "init": { - "data": { - "object_id": "Identyfikator obiektu" - }, - "description": "Prosz\u0119 wprowadzi\u0107 identyfikator obiektu (object_id) dla jednostki testowej.", - "title": "Wybierz identyfikator obiektu" - }, - "name": { - "data": { - "name": "Nazwa" - }, - "description": "Prosz\u0119 wprowadzi\u0107 nazw\u0119 dla jednostki testowej.", - "title": "Nazwa jednostki" - } - }, - "title": "Przyk\u0142ad wpisu do konfiguracji" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/ro.json b/homeassistant/components/config_entry_example/.translations/ro.json deleted file mode 100644 index 1a4cdd6bbb7..00000000000 --- a/homeassistant/components/config_entry_example/.translations/ro.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "step": { - "init": { - "description": "Introduce\u021bi un obiect_id pentru entitatea testat\u0103.", - "title": "Alege\u021bi id-ul obiectului" - }, - "name": { - "data": { - "name": "Nume" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/sl.json b/homeassistant/components/config_entry_example/.translations/sl.json deleted file mode 100644 index 11d2d3f5e80..00000000000 --- a/homeassistant/components/config_entry_example/.translations/sl.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "Neveljaven ID objekta" - }, - "step": { - "init": { - "data": { - "object_id": "ID objekta" - }, - "description": "Prosimo, vnesite Id_objekta za testni subjekt.", - "title": "Izberite ID objekta" - }, - "name": { - "data": { - "name": "Ime" - }, - "description": "Vnesite ime za testni subjekt.", - "title": "Ime subjekta" - } - }, - "title": "Primer nastavitve" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/vi.json b/homeassistant/components/config_entry_example/.translations/vi.json deleted file mode 100644 index e40c4d38e9f..00000000000 --- a/homeassistant/components/config_entry_example/.translations/vi.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng kh\u00f4ng h\u1ee3p l\u1ec7" - }, - "step": { - "init": { - "data": { - "object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng" - }, - "description": "Xin vui l\u00f2ng nh\u1eadp m\u1ed9t object_id cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", - "title": "Ch\u1ecdn id \u0111\u1ed1i t\u01b0\u1ee3ng" - }, - "name": { - "data": { - "name": "T\u00ean" - }, - "description": "Xin vui l\u00f2ng nh\u1eadp t\u00ean cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", - "title": "T\u00ean c\u1ee7a th\u1ef1c th\u1ec3" - } - }, - "title": "V\u00ed d\u1ee5 v\u1ec1 c\u1ea5u h\u00ecnh th\u1ef1c th\u1ec3" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/zh-Hans.json b/homeassistant/components/config_entry_example/.translations/zh-Hans.json deleted file mode 100644 index ee10e6d7b48..00000000000 --- a/homeassistant/components/config_entry_example/.translations/zh-Hans.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "\u65e0\u6548\u7684\u5bf9\u8c61 ID" - }, - "step": { - "init": { - "data": { - "object_id": "\u5bf9\u8c61 ID" - }, - "description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u5bf9\u8c61 ID", - "title": "\u8bf7\u9009\u62e9\u5bf9\u8c61 ID" - }, - "name": { - "data": { - "name": "\u540d\u79f0" - }, - "description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u540d\u79f0", - "title": "\u8bbe\u5907\u540d\u79f0" - } - }, - "title": "\u6837\u4f8b\u914d\u7f6e\u6761\u76ee" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/__init__.py b/homeassistant/components/config_entry_example/__init__.py deleted file mode 100644 index 3ebfdc3a183..00000000000 --- a/homeassistant/components/config_entry_example/__init__.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Example component to show how config entries work.""" - -import asyncio - -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import ATTR_FRIENDLY_NAME -from homeassistant.util import slugify - - -DOMAIN = 'config_entry_example' - - -@asyncio.coroutine -def async_setup(hass, config): - """Setup for our example component.""" - return True - - -@asyncio.coroutine -def async_setup_entry(hass, entry): - """Initialize an entry.""" - entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id']) - hass.states.async_set(entity_id, 'loaded', { - ATTR_FRIENDLY_NAME: entry.data['name'] - }) - - # Indicate setup was successful. - return True - - -@asyncio.coroutine -def async_unload_entry(hass, entry): - """Unload an entry.""" - entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id']) - hass.states.async_remove(entity_id) - - # Indicate unload was successful. - return True - - -@config_entries.HANDLERS.register(DOMAIN) -class ExampleConfigFlow(config_entries.ConfigFlowHandler): - """Handle an example configuration flow.""" - - VERSION = 1 - - def __init__(self): - """Initialize a Hue config handler.""" - self.object_id = None - - @asyncio.coroutine - def async_step_init(self, user_input=None): - """Start config flow.""" - errors = None - if user_input is not None: - object_id = user_input['object_id'] - - if object_id != '' and object_id == slugify(object_id): - self.object_id = user_input['object_id'] - return (yield from self.async_step_name()) - - errors = { - 'object_id': 'invalid_object_id' - } - - return self.async_show_form( - step_id='init', - data_schema=vol.Schema({ - 'object_id': str - }), - errors=errors - ) - - @asyncio.coroutine - def async_step_name(self, user_input=None): - """Ask user to enter the name.""" - errors = None - if user_input is not None: - name = user_input['name'] - - if name != '': - return self.async_create_entry( - title=name, - data={ - 'name': name, - 'object_id': self.object_id, - } - ) - - return self.async_show_form( - step_id='name', - data_schema=vol.Schema({ - 'name': str - }), - errors=errors - ) diff --git a/homeassistant/components/config_entry_example/strings.json b/homeassistant/components/config_entry_example/strings.json deleted file mode 100644 index a7a8cd4025b..00000000000 --- a/homeassistant/components/config_entry_example/strings.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "title": "Config Entry Example", - "step": { - "init": { - "title": "Pick object id", - "description": "Please enter an object_id for the test entity.", - "data": { - "object_id": "Object ID" - } - }, - "name": { - "title": "Name of the entity", - "description": "Please enter a name for the test entity.", - "data": { - "name": "Name" - } - } - }, - "error": { - "invalid_object_id": "Invalid object ID" - } - } -} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 6b2000b2ea6..69491af1aad 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -126,9 +126,8 @@ _LOGGER = logging.getLogger(__name__) HANDLERS = Registry() # Components that have config flows. In future we will auto-generate this list. FLOWS = [ - 'config_entry_example', - 'hue', 'deconz', + 'hue', ] SOURCE_USER = 'user' diff --git a/tests/components/test_config_entry_example.py b/tests/components/test_config_entry_example.py deleted file mode 100644 index 31084384c31..00000000000 --- a/tests/components/test_config_entry_example.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Test the config entry example component.""" -import asyncio - -from homeassistant import config_entries - - -@asyncio.coroutine -def test_flow_works(hass): - """Test that the config flow works.""" - result = yield from hass.config_entries.flow.async_init( - 'config_entry_example') - - assert result['type'] == config_entries.RESULT_TYPE_FORM - - result = yield from hass.config_entries.flow.async_configure( - result['flow_id'], { - 'object_id': 'bla' - }) - - assert result['type'] == config_entries.RESULT_TYPE_FORM - - result = yield from hass.config_entries.flow.async_configure( - result['flow_id'], { - 'name': 'Hello' - }) - - assert result['type'] == config_entries.RESULT_TYPE_CREATE_ENTRY - state = hass.states.get('config_entry_example.bla') - assert state is not None - assert state.name == 'Hello' - assert 'config_entry_example' in hass.config.components - assert len(hass.config_entries.async_entries()) == 1 - - # Test removing entry. - entry = hass.config_entries.async_entries()[0] - yield from hass.config_entries.async_remove(entry.entry_id) - state = hass.states.get('config_entry_example.bla') - assert state is None From 568c6c16fa72580ec0fd551661700fbad07c9b04 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Tue, 3 Apr 2018 19:05:06 -0400 Subject: [PATCH 101/136] Add missing service docs for hs_color (#13667) --- homeassistant/components/light/services.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 9645e50d06e..3507c6d2cda 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -15,6 +15,9 @@ turn_on: color_name: description: A human readable color name. example: 'red' + hs_color: + description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. + example: '[300, 70]' xy_color: description: Color for the light in XY-format. example: '[0.52, 0.43]' From 13bda2669eaf063b75bc23248edb4db18a7b7cb7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Apr 2018 16:49:13 -0700 Subject: [PATCH 102/136] Bump frontend to 20180404.0 --- 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 1fbfe94bb0d..3fc3eff0a14 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==20180401.0'] +REQUIREMENTS = ['home-assistant-frontend==20180404.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index a2e48d931c8..1cfe6df643f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -379,7 +379,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180401.0 +home-assistant-frontend==20180404.0 # homeassistant.components.homematicip_cloud homematicip==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3e9ae51a60..120d2c73024 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180401.0 +home-assistant-frontend==20180404.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 032d6963d841cea577495917c614dcded1986514 Mon Sep 17 00:00:00 2001 From: mountainsandcode Date: Wed, 4 Apr 2018 15:34:01 +0200 Subject: [PATCH 103/136] Add regex functions as templating helpers (#13631) * Add regex functions as templating helpers * Add regex functions as templating helpers - Style fixed * Templating filters, third time lucky? --- homeassistant/helpers/template.py | 37 +++++++++++++++++++++ tests/helpers/test_template.py | 53 +++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index a04023cfc4f..353fda28875 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -516,6 +516,39 @@ def forgiving_float(value): return value +def regex_match(value, find='', ignorecase=False): + """Match value using regex.""" + if not isinstance(value, str): + value = str(value) + flags = re.I if ignorecase else 0 + return bool(re.match(find, value, flags)) + + +def regex_replace(value='', find='', replace='', ignorecase=False): + """Replace using regex.""" + if not isinstance(value, str): + value = str(value) + flags = re.I if ignorecase else 0 + regex = re.compile(find, flags) + return regex.sub(replace, value) + + +def regex_search(value, find='', ignorecase=False): + """Search using regex.""" + if not isinstance(value, str): + value = str(value) + flags = re.I if ignorecase else 0 + return bool(re.search(find, value, flags)) + + +def regex_findall_index(value, find='', index=0, ignorecase=False): + """Find all matches using regex and then pick specific match index.""" + if not isinstance(value, str): + value = str(value) + flags = re.I if ignorecase else 0 + return re.findall(find, value, flags)[index] + + @contextfilter def random_every_time(context, values): """Choose a random value. @@ -545,6 +578,10 @@ ENV.filters['is_defined'] = fail_when_undefined ENV.filters['max'] = max ENV.filters['min'] = min ENV.filters['random'] = random_every_time +ENV.filters['regex_match'] = regex_match +ENV.filters['regex_replace'] = regex_replace +ENV.filters['regex_search'] = regex_search +ENV.filters['regex_findall_index'] = regex_findall_index ENV.globals['log'] = logarithm ENV.globals['float'] = forgiving_float ENV.globals['now'] = dt_util.now diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 693c3909924..650b98509d0 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -441,6 +441,59 @@ class TestHelpersTemplate(unittest.TestCase): template.Template('{{ utcnow().isoformat() }}', self.hass).render()) + def test_regex_match(self): + """Test regex_match method.""" + tpl = template.Template(""" +{{ '123-456-7890' | regex_match('(\d{3})-(\d{3})-(\d{4})') }} + """, self.hass) + self.assertEqual('True', tpl.render()) + + tpl = template.Template(""" +{{ 'home assistant test' | regex_match('Home', True) }} + """, self.hass) + self.assertEqual('True', tpl.render()) + + tpl = template.Template(""" + {{ 'Another home assistant test' | regex_match('home') }} + """, self.hass) + self.assertEqual('False', tpl.render()) + + def test_regex_search(self): + """Test regex_search method.""" + tpl = template.Template(""" +{{ '123-456-7890' | regex_search('(\d{3})-(\d{3})-(\d{4})') }} + """, self.hass) + self.assertEqual('True', tpl.render()) + + tpl = template.Template(""" +{{ 'home assistant test' | regex_search('Home', True) }} + """, self.hass) + self.assertEqual('True', tpl.render()) + + tpl = template.Template(""" + {{ 'Another home assistant test' | regex_search('home') }} + """, self.hass) + self.assertEqual('True', tpl.render()) + + def test_regex_replace(self): + """Test regex_replace method.""" + tpl = template.Template(""" +{{ 'Hello World' | regex_replace('(Hello\s)',) }} + """, self.hass) + self.assertEqual('World', tpl.render()) + + def test_regex_findall_index(self): + """Test regex_findall_index method.""" + tpl = template.Template(""" +{{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 0) }} + """, self.hass) + self.assertEqual('JFK', tpl.render()) + + tpl = template.Template(""" +{{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 1) }} + """, self.hass) + self.assertEqual('LHR', tpl.render()) + def test_distance_function_with_1_state(self): """Test distance function with 1 state.""" self.hass.states.set('test.object', 'happy', { From 4b2fdd243a68895fa36563f9f5cb0a6d4050cf82 Mon Sep 17 00:00:00 2001 From: stephanerosi Date: Wed, 4 Apr 2018 15:37:14 +0200 Subject: [PATCH 104/136] Channel up and down for webostv (#13624) --- homeassistant/components/media_player/webostv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index acd1ffad6eb..860d69e22c3 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -357,8 +357,8 @@ class LgWebOSDevice(MediaPlayerDevice): def media_next_track(self): """Send next track command.""" - self._client.fast_forward() + self._client.channel_up() def media_previous_track(self): """Send the previous track command.""" - self._client.rewind() + self._client.channel_down() From 9ce02d2717920374c9ea064004b14c866fa242e2 Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 4 Apr 2018 17:35:33 +0300 Subject: [PATCH 105/136] Added headers configuration variable to notify.rest component (#13674) * Added headers configuration variable to notify.rest component * Fix code style --- homeassistant/components/notify/rest.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/notify/rest.py b/homeassistant/components/notify/rest.py index 73618c19502..40b09dc3c72 100644 --- a/homeassistant/components/notify/rest.py +++ b/homeassistant/components/notify/rest.py @@ -12,7 +12,8 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, BaseNotificationService, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_RESOURCE, CONF_METHOD, CONF_NAME) +from homeassistant.const import (CONF_RESOURCE, CONF_METHOD, CONF_NAME, + CONF_HEADERS) import homeassistant.helpers.config_validation as cv CONF_DATA = 'data' @@ -29,6 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ default=DEFAULT_MESSAGE_PARAM_NAME): cv.string, vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(['POST', 'GET', 'POST_JSON']), + vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_TARGET_PARAMETER_NAME): cv.string, vol.Optional(CONF_TITLE_PARAMETER_NAME): cv.string, @@ -43,6 +45,7 @@ def get_service(hass, config, discovery_info=None): """Get the RESTful notification service.""" resource = config.get(CONF_RESOURCE) method = config.get(CONF_METHOD) + headers = config.get(CONF_HEADERS) message_param_name = config.get(CONF_MESSAGE_PARAMETER_NAME) title_param_name = config.get(CONF_TITLE_PARAMETER_NAME) target_param_name = config.get(CONF_TARGET_PARAMETER_NAME) @@ -50,19 +53,20 @@ def get_service(hass, config, discovery_info=None): data_template = config.get(CONF_DATA_TEMPLATE) return RestNotificationService( - hass, resource, method, message_param_name, + hass, resource, method, headers, message_param_name, title_param_name, target_param_name, data, data_template) class RestNotificationService(BaseNotificationService): """Implementation of a notification service for REST.""" - def __init__(self, hass, resource, method, message_param_name, + def __init__(self, hass, resource, method, headers, message_param_name, title_param_name, target_param_name, data, data_template): """Initialize the service.""" self._resource = resource self._hass = hass self._method = method.upper() + self._headers = headers self._message_param_name = message_param_name self._title_param_name = title_param_name self._target_param_name = target_param_name @@ -99,11 +103,14 @@ class RestNotificationService(BaseNotificationService): data.update(_data_template_creator(self._data_template)) if self._method == 'POST': - response = requests.post(self._resource, data=data, timeout=10) + response = requests.post(self._resource, headers=self._headers, + data=data, timeout=10) elif self._method == 'POST_JSON': - response = requests.post(self._resource, json=data, timeout=10) + response = requests.post(self._resource, headers=self._headers, + json=data, timeout=10) else: # default GET - response = requests.get(self._resource, params=data, timeout=10) + response = requests.get(self._resource, headers=self._headers, + params=data, timeout=10) if response.status_code not in (200, 201): _LOGGER.exception( From 415af5e2571969a9f813ee1fbcc08bd0df4c1a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 5 Apr 2018 00:30:02 +0300 Subject: [PATCH 106/136] Spelling fixes (#13681) --- homeassistant/components/alarm_control_panel/ifttt.py | 2 +- homeassistant/components/climate/nest.py | 2 +- homeassistant/components/device_tracker/ubus.py | 2 +- homeassistant/components/remote/xiaomi_miio.py | 2 +- homeassistant/components/smappee.py | 2 +- tests/components/homekit/test_accessories.py | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py index 5303c24876e..7bdc1ccd9d9 100644 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -93,7 +93,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class IFTTTAlarmPanel(alarm.AlarmControlPanel): - """Representation of an alarm control panel controlled throught IFTTT.""" + """Representation of an alarm control panel controlled through IFTTT.""" def __init__(self, name, code, event_away, event_home, event_night, event_disarm, optimistic): diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index e5c21158acb..d11f6890a7b 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -179,7 +179,7 @@ class NestThermostat(ClimateDevice): try: self.device.target = temp except nest.nest.APIError: - _LOGGER.error("An error occured while setting the temperature") + _LOGGER.error("An error occurred while setting the temperature") def set_operation_mode(self, operation_mode): """Set operation mode.""" diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index c75529655f4..dd12df7b070 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -95,7 +95,7 @@ class UbusDeviceScanner(DeviceScanner): return self.last_results def _generate_mac2name(self): - """Return empty MAC to name dict. Overriden if DHCP server is set.""" + """Return empty MAC to name dict. Overridden if DHCP server is set.""" self.mac2name = dict() @_refresh_on_access_denied diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index b71eb2cb447..e731d421e69 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -138,7 +138,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): while (utcnow() - start_time) < timedelta(seconds=timeout): message = yield from hass.async_add_job( device.read, slot) - _LOGGER.debug("Message recieved from device: '%s'", message) + _LOGGER.debug("Message received from device: '%s'", message) if 'code' in message and message['code']: log_msg = "Received command is: {}".format(message['code']) diff --git a/homeassistant/components/smappee.py b/homeassistant/components/smappee.py index 0111e0437fb..1241679770b 100644 --- a/homeassistant/components/smappee.py +++ b/homeassistant/components/smappee.py @@ -264,7 +264,7 @@ class Smappee(object): return True def active_power(self): - """Get sum of all instantanious active power values from local hub.""" + """Get sum of all instantaneous active power values from local hub.""" if not self.is_local_active: return diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 4d230b81686..6599ec83335 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -153,13 +153,13 @@ class TestAccessories(unittest.TestCase): def test_home_driver(self): """Test HomeDriver class.""" bridge = HomeBridge(None) - ip_adress = '127.0.0.1' + ip_address = '127.0.0.1' port = 51826 path = '.homekit.state' with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ as mock_driver: - HomeDriver(bridge, ip_adress, port, path) + HomeDriver(bridge, ip_address, port, path) self.assertEqual( - mock_driver.call_args, call(bridge, ip_adress, port, path)) + mock_driver.call_args, call(bridge, ip_address, port, path)) From 301077ded9dcaebf9b1e45561a8b1871d9895441 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 5 Apr 2018 00:42:00 +0200 Subject: [PATCH 107/136] Xiaomi MiIO Light: White Philips Candle Light support (#13682) --- homeassistant/components/light/xiaomi_miio.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 21a27c33203..24eab7ebd4a 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -38,6 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.ceiling', 'philips.light.zyceiling', 'philips.light.bulb', + 'philips.light.candle', 'philips.light.candle2']), }) @@ -149,7 +150,9 @@ async def async_setup_platform(hass, config, async_add_devices, device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device - elif model in ['philips.light.bulb', 'philips.light.candle2']: + elif model in ['philips.light.bulb', + 'philips.light.candle', + 'philips.light.candle2']: from miio import PhilipsBulb light = PhilipsBulb(host, token) device = XiaomiPhilipsBulb(name, light, model, unique_id) From f263a931f7ac3806ac914b6cdc2be3bc0dbf0734 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 5 Apr 2018 00:46:27 +0200 Subject: [PATCH 108/136] Bugfixes HomeKit covers, lights (#13689) * covers -> current_position attribute * lights -> hue and saturation attribute --- .../components/homekit/type_covers.py | 19 ++++++++----------- .../components/homekit/type_lights.py | 4 +++- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 7616ef05fdf..9a526508117 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -63,14 +63,11 @@ class WindowCovering(HomeAccessory): return current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) - if current_position is None: - return - - self.current_position = int(current_position) - self.char_current_position.set_value(self.current_position) - - if self.homekit_target is None or \ - abs(self.current_position - self.homekit_target) < 6: - self.char_target_position.set_value(self.current_position) - self.char_position_state.set_value(2) - self.homekit_target = None + if isinstance(current_position, int): + self.current_position = current_position + self.char_current_position.set_value(self.current_position) + if self.homekit_target is None or \ + abs(self.current_position - self.homekit_target) < 6: + self.char_target_position.set_value(self.current_position) + self.char_position_state.set_value(2) + self.homekit_target = None diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index d88e7100131..d5b967797bb 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -146,7 +146,9 @@ class Light(HomeAccessory): hue, saturation = new_state.attributes.get( ATTR_HS_COLOR, (None, None)) if not self._flag[RGB_COLOR] and ( - hue != self._hue or saturation != self._saturation): + hue != self._hue or saturation != self._saturation) and \ + isinstance(hue, (int, float)) and \ + isinstance(saturation, (int, float)): self.char_hue.set_value(hue, should_callback=False) self.char_saturation.set_value(saturation, should_callback=False) From 692b2644c7e902a9aeddc53f99cafe0543d0d89c Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 5 Apr 2018 00:52:25 +0200 Subject: [PATCH 109/136] Minor style changes, cleanup (#13654) * Minor style changes, cleanup * Change 'self._entity.id' to 'self.entity_id' * Use const 'STATE_OFF' * Added CATEGORY constants * Removed *args from accessory types * Changed 'self._hass' to 'self.hass' * Added log debug msg (for added lights) --- homeassistant/components/homekit/__init__.py | 4 +- .../components/homekit/accessories.py | 10 ++--- homeassistant/components/homekit/const.py | 4 ++ .../components/homekit/type_covers.py | 20 +++++----- .../components/homekit/type_lights.py | 34 ++++++++-------- .../homekit/type_security_systems.py | 23 +++++------ .../components/homekit/type_sensors.py | 16 ++++---- .../components/homekit/type_switches.py | 18 ++++----- .../components/homekit/type_thermostats.py | 40 +++++++++---------- .../homekit/test_type_thermostats.py | 5 +-- 10 files changed, 87 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 8ef8445aa70..25b54b6c723 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -102,8 +102,7 @@ def get_accessory(hass, state, aid, config): aid=aid) elif state.domain == 'alarm_control_panel': - _LOGGER.debug('Add "%s" as "%s"', state.entity_id, - 'SecuritySystem') + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem') return TYPES['SecuritySystem'](hass, state.entity_id, state.name, alarm_code=config.get(ATTR_CODE), aid=aid) @@ -120,6 +119,7 @@ def get_accessory(hass, state, aid, config): state.name, support_auto, aid=aid) elif state.domain == 'light': + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light') return TYPES['Light'](hass, state.entity_id, state.name, aid=aid) elif state.domain == 'switch' or state.domain == 'remote' \ diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 4c4409e6dfc..3e9f22ef2bc 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -65,10 +65,10 @@ class HomeAccessory(Accessory): def run(self): """Method called by accessory after driver is started.""" - state = self._hass.states.get(self._entity_id) + state = self.hass.states.get(self.entity_id) self.update_state(new_state=state) async_track_state_change( - self._hass, self._entity_id, self.update_state) + self.hass, self.entity_id, self.update_state) class HomeBridge(Bridge): @@ -79,7 +79,7 @@ class HomeBridge(Bridge): """Initialize a Bridge object.""" super().__init__(name, **kwargs) set_accessory_info(self, name, model) - self._hass = hass + self.hass = hass def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) @@ -92,12 +92,12 @@ class HomeBridge(Bridge): def add_paired_client(self, client_uuid, client_public): """Override super function to dismiss setup message if paired.""" super().add_paired_client(client_uuid, client_public) - dismiss_setup_message(self._hass) + dismiss_setup_message(self.hass) def remove_paired_client(self, client_uuid): """Override super function to show setup message if unpaired.""" super().remove_paired_client(client_uuid) - show_setup_message(self, self._hass) + show_setup_message(self, self.hass) class HomeDriver(AccessoryDriver): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index a45c8298b78..c2a10f61fcb 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -24,8 +24,12 @@ BRIDGE_NAME = 'Home Assistant' MANUFACTURER = 'HomeAssistant' # #### Categories #### +CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM' CATEGORY_LIGHT = 'LIGHTBULB' CATEGORY_SENSOR = 'SENSOR' +CATEGORY_SWITCH = 'SWITCH' +CATEGORY_THERMOSTAT = 'THERMOSTAT' +CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING' # #### Services #### diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 9a526508117..3650a948f5d 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -6,8 +6,8 @@ from homeassistant.components.cover import ATTR_CURRENT_POSITION from . import TYPES from .accessories import HomeAccessory, add_preload_service from .const import ( - SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, - CHAR_TARGET_POSITION, CHAR_POSITION_STATE) + CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING, + CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE) _LOGGER = logging.getLogger(__name__) @@ -20,13 +20,13 @@ class WindowCovering(HomeAccessory): The cover entity must support: set_cover_position. """ - def __init__(self, hass, entity_id, display_name, *args, **kwargs): + def __init__(self, hass, entity_id, display_name, **kwargs): """Initialize a WindowCovering accessory object.""" - super().__init__(display_name, entity_id, 'WINDOW_COVERING', - *args, **kwargs) + super().__init__(display_name, entity_id, + CATEGORY_WINDOW_COVERING, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id self.current_position = None self.homekit_target = None @@ -48,14 +48,14 @@ class WindowCovering(HomeAccessory): """Move cover to value if call came from HomeKit.""" self.char_target_position.set_value(value, should_callback=False) if value != self.current_position: - _LOGGER.debug('%s: Set position to %d', self._entity_id, value) + _LOGGER.debug('%s: Set position to %d', self.entity_id, value) self.homekit_target = value if value > self.current_position: self.char_position_state.set_value(1) elif value < self.current_position: self.char_position_state.set_value(0) - self._hass.components.cover.set_cover_position( - value, self._entity_id) + self.hass.components.cover.set_cover_position( + value, self.entity_id) def update_state(self, entity_id=None, old_state=None, new_state=None): """Update cover position after state changed.""" diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index d5b967797bb..b02aee1e714 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -23,19 +23,19 @@ class Light(HomeAccessory): Currently supports: state, brightness, rgb_color. """ - def __init__(self, hass, entity_id, name, *args, **kwargs): + def __init__(self, hass, entity_id, name, **kwargs): """Initialize a new Light accessory object.""" - super().__init__(name, entity_id, CATEGORY_LIGHT, *args, **kwargs) + super().__init__(name, entity_id, CATEGORY_LIGHT, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: False} self._state = 0 self.chars = [] - self._features = self._hass.states.get(self._entity_id) \ + self._features = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_SUPPORTED_FEATURES) if self._features & SUPPORT_BRIGHTNESS: self.chars.append(CHAR_BRIGHTNESS) @@ -70,29 +70,29 @@ class Light(HomeAccessory): if self._state == value: return - _LOGGER.debug('%s: Set state to %d', self._entity_id, value) + _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self._flag[CHAR_ON] = True self.char_on.set_value(value, should_callback=False) if value == 1: - self._hass.components.light.turn_on(self._entity_id) + self.hass.components.light.turn_on(self.entity_id) elif value == 0: - self._hass.components.light.turn_off(self._entity_id) + self.hass.components.light.turn_off(self.entity_id) def set_brightness(self, value): """Set brightness if call came from HomeKit.""" - _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) + _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value) self._flag[CHAR_BRIGHTNESS] = True self.char_brightness.set_value(value, should_callback=False) if value != 0: - self._hass.components.light.turn_on( - self._entity_id, brightness_pct=value) + self.hass.components.light.turn_on( + self.entity_id, brightness_pct=value) else: - self._hass.components.light.turn_off(self._entity_id) + self.hass.components.light.turn_off(self.entity_id) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" - _LOGGER.debug('%s: Set saturation to %d', self._entity_id, value) + _LOGGER.debug('%s: Set saturation to %d', self.entity_id, value) self._flag[CHAR_SATURATION] = True self.char_saturation.set_value(value, should_callback=False) self._saturation = value @@ -100,7 +100,7 @@ class Light(HomeAccessory): def set_hue(self, value): """Set hue if call came from HomeKit.""" - _LOGGER.debug('%s: Set hue to %d', self._entity_id, value) + _LOGGER.debug('%s: Set hue to %d', self.entity_id, value) self._flag[CHAR_HUE] = True self.char_hue.set_value(value, should_callback=False) self._hue = value @@ -112,11 +112,11 @@ class Light(HomeAccessory): if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \ self._flag[CHAR_SATURATION]: color = (self._hue, self._saturation) - _LOGGER.debug('%s: Set hs_color to %s', self._entity_id, color) + _LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color) self._flag.update({ CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) - self._hass.components.light.turn_on( - self._entity_id, hs_color=color) + self.hass.components.light.turn_on( + self.entity_id, hs_color=color) def update_state(self, entity_id=None, old_state=None, new_state=None): """Update light after state change.""" diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index b23522f0ea2..2cce6653db3 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -9,8 +9,8 @@ from homeassistant.const import ( from . import TYPES from .accessories import HomeAccessory, add_preload_service from .const import ( - SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE, - CHAR_TARGET_SECURITY_STATE) + CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM, + CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE) _LOGGER = logging.getLogger(__name__) @@ -27,14 +27,13 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm', class SecuritySystem(HomeAccessory): """Generate an SecuritySystem accessory for an alarm control panel.""" - def __init__(self, hass, entity_id, display_name, - alarm_code, *args, **kwargs): + def __init__(self, hass, entity_id, display_name, alarm_code, **kwargs): """Initialize a SecuritySystem accessory object.""" - super().__init__(display_name, entity_id, 'ALARM_SYSTEM', - *args, **kwargs) + super().__init__(display_name, entity_id, + CATEGORY_ALARM_SYSTEM, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id self._alarm_code = alarm_code self.flag_target_state = False @@ -52,16 +51,16 @@ class SecuritySystem(HomeAccessory): def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set security state to %d', - self._entity_id, value) + self.entity_id, value) self.flag_target_state = True self.char_target_state.set_value(value, should_callback=False) hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] - params = {ATTR_ENTITY_ID: self._entity_id} + params = {ATTR_ENTITY_ID: self.entity_id} if self._alarm_code: params[ATTR_CODE] = self._alarm_code - self._hass.services.call('alarm_control_panel', service, params) + self.hass.services.call('alarm_control_panel', service, params) def update_state(self, entity_id=None, old_state=None, new_state=None): """Update security state after state changed.""" @@ -76,7 +75,7 @@ class SecuritySystem(HomeAccessory): self.char_current_state.set_value(current_security_state, should_callback=False) _LOGGER.debug('%s: Updated current state to %s (%d)', - self._entity_id, hass_state, current_security_state) + self.entity_id, hass_state, current_security_state) if not self.flag_target_state: self.char_target_state.set_value(current_security_state, diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index e980ce4a316..9768c4a51d4 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -23,12 +23,12 @@ class TemperatureSensor(HomeAccessory): Sensor entity must return temperature in °C, °F. """ - def __init__(self, hass, entity_id, name, *args, **kwargs): + def __init__(self, hass, entity_id, name, **kwargs): """Initialize a TemperatureSensor accessory object.""" - super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) + super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE) @@ -47,7 +47,7 @@ class TemperatureSensor(HomeAccessory): temperature = temperature_to_homekit(temperature, unit) self.char_temp.set_value(temperature, should_callback=False) _LOGGER.debug('%s: Current temperature set to %d°C', - self._entity_id, temperature) + self.entity_id, temperature) @TYPES.register('HumiditySensor') @@ -58,8 +58,8 @@ class HumiditySensor(HomeAccessory): """Initialize a HumiditySensor accessory object.""" super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR) self.char_humidity = serv_humidity \ @@ -75,4 +75,4 @@ class HumiditySensor(HomeAccessory): if humidity: self.char_humidity.set_value(humidity, should_callback=False) _LOGGER.debug('%s: Percent set to %d%%', - self._entity_id, humidity) + self.entity_id, humidity) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 1f19893d0be..689edde6f37 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -7,7 +7,7 @@ from homeassistant.core import split_entity_id from . import TYPES from .accessories import HomeAccessory, add_preload_service -from .const import SERV_SWITCH, CHAR_ON +from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON _LOGGER = logging.getLogger(__name__) @@ -16,12 +16,12 @@ _LOGGER = logging.getLogger(__name__) class Switch(HomeAccessory): """Generate a Switch accessory.""" - def __init__(self, hass, entity_id, display_name, *args, **kwargs): + def __init__(self, hass, entity_id, display_name, **kwargs): """Initialize a Switch accessory object to represent a remote.""" - super().__init__(display_name, entity_id, 'SWITCH', *args, **kwargs) + super().__init__(display_name, entity_id, CATEGORY_SWITCH, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id self._domain = split_entity_id(entity_id)[0] self.flag_target_state = False @@ -34,12 +34,12 @@ class Switch(HomeAccessory): def set_state(self, value): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state to %s', - self._entity_id, value) + self.entity_id, value) self.flag_target_state = True self.char_on.set_value(value, should_callback=False) service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF - self._hass.services.call(self._domain, service, - {ATTR_ENTITY_ID: self._entity_id}) + self.hass.services.call(self._domain, service, + {ATTR_ENTITY_ID: self.entity_id}) def update_state(self, entity_id=None, old_state=None, new_state=None): """Update switch state after state changed.""" @@ -49,7 +49,7 @@ class Switch(HomeAccessory): current_state = (new_state.state == STATE_ON) if not self.flag_target_state: _LOGGER.debug('%s: Set current state to %s', - self._entity_id, current_state) + self.entity_id, current_state) self.char_on.set_value(current_state, should_callback=False) self.flag_target_state = False diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index d49c1ca626b..69b61062791 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -7,12 +7,12 @@ from homeassistant.components.climate import ( ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_HEAT, STATE_COOL, STATE_AUTO) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES from .accessories import HomeAccessory, add_preload_service from .const import ( - SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, + CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) @@ -20,7 +20,6 @@ from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) -STATE_OFF = 'off' UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1} UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()} HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1, @@ -32,14 +31,13 @@ HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} class Thermostat(HomeAccessory): """Generate a Thermostat accessory for a climate.""" - def __init__(self, hass, entity_id, display_name, - support_auto, *args, **kwargs): + def __init__(self, hass, entity_id, display_name, support_auto, **kwargs): """Initialize a Thermostat accessory object.""" - super().__init__(display_name, entity_id, 'THERMOSTAT', - *args, **kwargs) + super().__init__(display_name, entity_id, + CATEGORY_THERMOSTAT, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id self._call_timer = None self._unit = TEMP_CELSIUS @@ -101,48 +99,48 @@ class Thermostat(HomeAccessory): """Move operation mode to value if call came from HomeKit.""" self.char_target_heat_cool.set_value(value, should_callback=False) if value in HC_HOMEKIT_TO_HASS: - _LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value) + _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) self.heat_cool_flag_target_state = True hass_value = HC_HOMEKIT_TO_HASS[value] - self._hass.components.climate.set_operation_mode( - operation_mode=hass_value, entity_id=self._entity_id) + self.hass.components.climate.set_operation_mode( + operation_mode=hass_value, entity_id=self.entity_id) def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" _LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C', - self._entity_id, value) + self.entity_id, value) self.coolingthresh_flag_target_state = True self.char_cooling_thresh_temp.set_value(value, should_callback=False) low = self.char_heating_thresh_temp.value low = temperature_to_states(low, self._unit) value = temperature_to_states(value, self._unit) - self._hass.components.climate.set_temperature( - entity_id=self._entity_id, target_temp_high=value, + self.hass.components.climate.set_temperature( + entity_id=self.entity_id, target_temp_high=value, target_temp_low=low) def set_heating_threshold(self, value): """Set heating threshold temp to value if call came from HomeKit.""" _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', - self._entity_id, value) + self.entity_id, value) self.heatingthresh_flag_target_state = True self.char_heating_thresh_temp.set_value(value, should_callback=False) # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value high = temperature_to_states(high, self._unit) value = temperature_to_states(value, self._unit) - self._hass.components.climate.set_temperature( - entity_id=self._entity_id, target_temp_high=high, + self.hass.components.climate.set_temperature( + entity_id=self.entity_id, target_temp_high=high, target_temp_low=value) def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" _LOGGER.debug('%s: Set target temperature to %.2f°C', - self._entity_id, value) + self.entity_id, value) self.temperature_flag_target_state = True self.char_target_temp.set_value(value, should_callback=False) value = temperature_to_states(value, self._unit) - self._hass.components.climate.set_temperature( - temperature=value, entity_id=self._entity_id) + self.hass.components.climate.set_temperature( + temperature=value, entity_id=self.entity_id) def update_state(self, entity_id=None, old_state=None, new_state=None): """Update security state after state changed.""" diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 011fe73377d..e1511163f2f 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -6,11 +6,10 @@ from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) -from homeassistant.components.homekit.type_thermostats import ( - Thermostat, STATE_OFF) +from homeassistant.components.homekit.type_thermostats import Thermostat from homeassistant.const import ( ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA, - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant From fe56844a3aa70ad7c63d19b9562b784cfc667900 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Thu, 5 Apr 2018 11:14:15 +0200 Subject: [PATCH 110/136] Bugfix: Zwave Print_node to logfile instead of console (#13302) * Print to logfile instead of console * Review changes * Typo --- homeassistant/components/zwave/__init__.py | 6 ++---- tests/components/zwave/test_init.py | 16 +++++++--------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index a85160e8bde..02d2b574592 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -182,10 +182,8 @@ def nice_print_node(node): node_dict['values'] = {value_id: _obj_to_dict(value) for value_id, value in node.values.items()} - print("\n\n\n") - print("FOUND NODE", node.product_name) - pprint(node_dict) - print("\n\n\n") + _LOGGER.info("FOUND NODE %s \n" + "%s", node.product_name, node_dict) def get_config_value(node, value_index, tries=5): diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index bb073459b48..004e5e95ca0 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -1094,20 +1094,18 @@ class TestZWaveServices(unittest.TestCase): assert mock_logger.info.mock_calls[0][1][3] == 2345 def test_print_node(self): - """Test zwave print_config_parameter service.""" - node1 = MockNode(node_id=14) - node2 = MockNode(node_id=15) - self.zwave_network.nodes = {14: node1, 15: node2} + """Test zwave print_node_parameter service.""" + node = MockNode(node_id=14) - with patch.object(zwave, 'pprint') as mock_pprint: + self.zwave_network.nodes = {14: node} + + with self.assertLogs(level='INFO') as mock_logger: self.hass.services.call('zwave', 'print_node', { - const.ATTR_NODE_ID: 15, + const.ATTR_NODE_ID: 14 }) self.hass.block_till_done() - assert mock_pprint.called - assert len(mock_pprint.mock_calls) == 1 - assert mock_pprint.mock_calls[0][1][0]['node_id'] == 15 + self.assertIn("FOUND NODE ", mock_logger.output[1]) def test_set_wakeup(self): """Test zwave set_wakeup service.""" From 206e38a2aba3eda32a30cdac54976c6c1156678a Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 5 Apr 2018 13:20:20 +0200 Subject: [PATCH 111/136] Update HAP-python to 1.1.8 (#13563) * Bump version to HAP-python==1.1.8 * Required changes for version change * Small bugfix lights --- homeassistant/components/homekit/__init__.py | 2 +- .../components/homekit/accessories.py | 14 ++------ homeassistant/components/homekit/const.py | 5 --- .../components/homekit/type_lights.py | 1 + .../components/homekit/type_sensors.py | 5 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_accessories.py | 32 ++++--------------- 8 files changed, 14 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 25b54b6c723..948e26be291 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -28,7 +28,7 @@ from .util import ( TYPES = Registry() _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==1.1.7'] +REQUIREMENTS = ['HAP-python==1.1.8'] CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 3e9f22ef2bc..da45bee9e90 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -8,8 +8,8 @@ from homeassistant.helpers.event import async_track_state_change from .const import ( ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, - MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, - CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) + MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL, + CHAR_NAME, CHAR_SERIAL_NUMBER) from .util import ( show_setup_message, dismiss_setup_message) @@ -39,15 +39,6 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) -def override_properties(char, properties=None, valid_values=None): - """Override characteristic property values and valid values.""" - if properties: - char.properties.update(properties) - - if valid_values: - char.properties['ValidValues'].update(valid_values) - - class HomeAccessory(Accessory): """Adapter class for Accessory.""" @@ -83,7 +74,6 @@ class HomeBridge(Bridge): def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) - add_preload_service(self, SERV_BRIDGING_STATE) def setup_message(self): """Prevent print of pyhap setup message to terminal.""" diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index c2a10f61fcb..676f83bf8e8 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -34,7 +34,6 @@ CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING' # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' -SERV_BRIDGING_STATE = 'BridgingState' SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered, # StatusLowBattery, Name @@ -47,9 +46,7 @@ SERV_WINDOW_COVERING = 'WindowCovering' # #### Characteristics #### -CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier' CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] -CHAR_CATEGORY = 'Category' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_POSITION = 'CurrentPosition' @@ -58,13 +55,11 @@ CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' # arcdegress | [0, 360] -CHAR_LINK_QUALITY = 'LinkQuality' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' CHAR_NAME = 'Name' CHAR_ON = 'On' # boolean CHAR_POSITION_STATE = 'PositionState' -CHAR_REACHABLE = 'Reachable' CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index b02aee1e714..45ed9405a2a 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -152,4 +152,5 @@ class Light(HomeAccessory): self.char_hue.set_value(hue, should_callback=False) self.char_saturation.set_value(saturation, should_callback=False) + self._hue, self._saturation = (hue, saturation) self._flag[RGB_COLOR] = False diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 9768c4a51d4..80521df5991 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -5,8 +5,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from . import TYPES -from .accessories import ( - HomeAccessory, add_preload_service, override_properties) +from .accessories import HomeAccessory, add_preload_service from .const import ( CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) @@ -32,7 +31,7 @@ class TemperatureSensor(HomeAccessory): serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE) - override_properties(self.char_temp, PROP_CELSIUS) + self.char_temp.override_properties(properties=PROP_CELSIUS) self.char_temp.value = 0 self.unit = None diff --git a/requirements_all.txt b/requirements_all.txt index 1cfe6df643f..49b23015794 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ attrs==17.4.0 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==1.1.7 +HAP-python==1.1.8 # homeassistant.components.notify.mastodon Mastodon.py==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 120d2c73024..7c5467f7608 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.4 # homeassistant.components.homekit -HAP-python==1.1.7 +HAP-python==1.1.8 # homeassistant.components.notify.html5 PyJWT==1.6.0 diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 6599ec83335..a2facd826e4 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -6,12 +6,12 @@ import unittest from unittest.mock import call, patch, Mock from homeassistant.components.homekit.accessories import ( - add_preload_service, set_accessory_info, override_properties, + add_preload_service, set_accessory_info, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, - SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, - CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) + SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL, + CHAR_NAME, CHAR_SERIAL_NUMBER) class TestAccessories(unittest.TestCase): @@ -22,7 +22,7 @@ class TestAccessories(unittest.TestCase): acc = Mock() serv = add_preload_service(acc, 'AirPurifier') self.assertEqual(acc.mock_calls, [call.add_service(serv)]) - with self.assertRaises(AssertionError): + with self.assertRaises(ValueError): serv.get_characteristic('Name') # Test with typo in service name @@ -68,24 +68,6 @@ class TestAccessories(unittest.TestCase): self.assertEqual( serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') - def test_override_properties(self): - """Test overriding property values.""" - serv = add_preload_service(Mock(), 'AirPurifier', 'RotationSpeed') - - char_active = serv.get_characteristic('Active') - char_rotation_speed = serv.get_characteristic('RotationSpeed') - - self.assertTrue( - char_active.properties['ValidValues'].get('State') is None) - self.assertEqual(char_rotation_speed.properties['maxValue'], 100) - - override_properties(char_active, valid_values={'State': 'On'}) - override_properties(char_rotation_speed, properties={'maxValue': 200}) - - self.assertFalse( - char_active.properties['ValidValues'].get('State') is None) - self.assertEqual(char_rotation_speed.properties['maxValue'], 200) - def test_home_accessory(self): """Test HomeAccessory class.""" acc = HomeAccessory() @@ -110,17 +92,15 @@ class TestAccessories(unittest.TestCase): bridge = HomeBridge(None) self.assertEqual(bridge.display_name, BRIDGE_NAME) self.assertEqual(bridge.category, 2) # Category.BRIDGE - self.assertEqual(len(bridge.services), 2) + self.assertEqual(len(bridge.services), 1) serv = bridge.services[0] # SERV_ACCESSORY_INFO self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) self.assertEqual( serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL) - serv = bridge.services[1] # SERV_BRIDGING_STATE - self.assertEqual(serv.display_name, SERV_BRIDGING_STATE) bridge = HomeBridge('hass', 'test_name', 'test_model') self.assertEqual(bridge.display_name, 'test_name') - self.assertEqual(len(bridge.services), 2) + self.assertEqual(len(bridge.services), 1) serv = bridge.services[0] # SERV_ACCESSORY_INFO self.assertEqual( serv.get_characteristic(CHAR_MODEL).value, 'test_model') From b2d37f525703f21ff929f469bd66aec66ca40405 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Thu, 5 Apr 2018 17:54:17 +0200 Subject: [PATCH 112/136] Update ha-philips_js to 0.0.3 (#13702) * Update requirements_all.txt * Update philips_js.py --- homeassistant/components/media_player/philips_js.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index 29d336e4d7a..d526fbb0387 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -19,7 +19,7 @@ from homeassistant.const import ( from homeassistant.helpers.script import Script from homeassistant.util import Throttle -REQUIREMENTS = ['ha-philipsjs==0.0.2'] +REQUIREMENTS = ['ha-philipsjs==0.0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 49b23015794..1b581cd2040 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -358,7 +358,7 @@ gstreamer-player==1.1.0 ha-ffmpeg==1.9 # homeassistant.components.media_player.philips_js -ha-philipsjs==0.0.2 +ha-philipsjs==0.0.3 # homeassistant.components.sensor.geo_rss_events haversine==0.4.5 From 1b3c3494e8e7e4d11024b549979632839cccde6e Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Thu, 5 Apr 2018 17:58:55 +0200 Subject: [PATCH 113/136] Coverage & Codeowners (#13700) --- .coveragerc | 17 ++++++----------- CODEOWNERS | 15 ++++++++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.coveragerc b/.coveragerc index 828da909a06..79b1cffa6fe 100644 --- a/.coveragerc +++ b/.coveragerc @@ -159,7 +159,7 @@ omit = homeassistant/components/maxcube.py homeassistant/components/*/maxcube.py - + homeassistant/components/mochad.py homeassistant/components/*/mochad.py @@ -286,11 +286,9 @@ omit = homeassistant/components/*/wink.py homeassistant/components/xiaomi_aqara.py - homeassistant/components/binary_sensor/xiaomi_aqara.py - homeassistant/components/cover/xiaomi_aqara.py - homeassistant/components/light/xiaomi_aqara.py - homeassistant/components/sensor/xiaomi_aqara.py - homeassistant/components/switch/xiaomi_aqara.py + homeassistant/components/*/xiaomi_aqara.py + + homeassistant/components/*/xiaomi_miio.py homeassistant/components/zabbix.py homeassistant/components/*/zabbix.py @@ -398,7 +396,6 @@ omit = homeassistant/components/emoncms_history.py homeassistant/components/emulated_hue/upnp.py homeassistant/components/fan/mqtt.py - homeassistant/components/fan/xiaomi_miio.py homeassistant/components/feedreader.py homeassistant/components/folder_watcher.py homeassistant/components/foursquare.py @@ -431,7 +428,6 @@ omit = homeassistant/components/light/tplink.py homeassistant/components/light/tradfri.py homeassistant/components/light/x10.py - homeassistant/components/light/xiaomi_miio.py homeassistant/components/light/yeelight.py homeassistant/components/light/yeelightsunflower.py homeassistant/components/light/zengge.py @@ -440,6 +436,7 @@ omit = homeassistant/components/lock/nello.py homeassistant/components/lock/nuki.py homeassistant/components/lock/sesame.py + homeassistant/components/map.py homeassistant/components/media_extractor.py homeassistant/components/media_player/anthemav.py homeassistant/components/media_player/aquostv.py @@ -538,7 +535,6 @@ omit = homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote/harmony.py homeassistant/components/remote/itach.py - homeassistant/components/remote/xiaomi_miio.py homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/lifx_cloud.py homeassistant/components/sensor/airvisual.py @@ -674,6 +670,7 @@ omit = homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/viaggiatreno.py homeassistant/components/sensor/waqi.py + homeassistant/components/sensor/waze_travel_time.py homeassistant/components/sensor/whois.py homeassistant/components/sensor/worldtidesinfo.py homeassistant/components/sensor/worxlandroid.py @@ -707,7 +704,6 @@ omit = homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py homeassistant/components/switch/vesync.py - homeassistant/components/switch/xiaomi_miio.py homeassistant/components/telegram_bot/* homeassistant/components/thingspeak.py homeassistant/components/tts/amazon_polly.py @@ -716,7 +712,6 @@ omit = homeassistant/components/tts/picotts.py homeassistant/components/vacuum/mqtt.py homeassistant/components/vacuum/roomba.py - homeassistant/components/vacuum/xiaomi_miio.py homeassistant/components/weather/bom.py homeassistant/components/weather/buienradar.py homeassistant/components/weather/darksky.py diff --git a/CODEOWNERS b/CODEOWNERS index 932f07573b2..67aef6a248f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -68,8 +68,9 @@ homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel homeassistant/components/sensor/pollen.py @bachya homeassistant/components/sensor/qnap.py @colinodell -homeassistant/components/sensor/sytadin.py @gautric +homeassistant/components/sensor/sma.py @kellerza homeassistant/components/sensor/sql.py @dgomes +homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/switch/rainmachine.py @bachya @@ -79,17 +80,17 @@ homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/axis.py @kane610 homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/broadlink.py @danielhiversen +homeassistant/components/*/deconz.py @kane610 homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64 homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p -homeassistant/components/*/deconz.py @kane610 -homeassistant/components/*/rfxtrx.py @danielhiversen -homeassistant/components/velux.py @Julius2342 -homeassistant/components/*/velux.py @Julius2342 homeassistant/components/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342 +homeassistant/components/qwikswitch.py @kellerza +homeassistant/components/*/qwikswitch.py @kellerza +homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei homeassistant/components/tesla.py @zabuldon @@ -97,5 +98,9 @@ homeassistant/components/*/tesla.py @zabuldon homeassistant/components/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tradfri.py @ggravlingen +homeassistant/components/velux.py @Julius2342 +homeassistant/components/*/velux.py @Julius2342 homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi + +homeassistant/scripts/check_config.py @kellerza From 61a3b4ffdb7f7e40e000d4ca51b3db58811b260d Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 5 Apr 2018 11:59:32 -0400 Subject: [PATCH 114/136] Bump insteonplm to 0.8.6 to fix sensor message handling (#13691) --- homeassistant/components/insteon_plm.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py index 6f5c5223ea0..d867f0c3d28 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.8.3'] +REQUIREMENTS = ['insteonplm==0.8.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1b581cd2040..9a0613211da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -436,7 +436,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.8.3 +insteonplm==0.8.6 # homeassistant.components.verisure jsonpath==0.75 From 63820a78d95f364581f7d7f175a000eeb2030e29 Mon Sep 17 00:00:00 2001 From: shuaiger Date: Fri, 6 Apr 2018 00:00:40 +0800 Subject: [PATCH 115/136] Fix asuswrt ap mode failure (#13693) * fix asuswrt ap mode failure When using ap mode, asuswrt device_tracker does dont work properly as ip can not be retrieved from wl command. This version fixed the issue. * save 1 line code * another 2 lines saved * typo correction --- homeassistant/components/device_tracker/asuswrt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 14aea561c8e..5b8e173aba4 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -172,7 +172,7 @@ class AsusWrtDeviceScanner(DeviceScanner): ret_devices = {} for key in devices: - if devices[key].ip is not None: + if self.mode == 'ap' or devices[key].ip is not None: ret_devices[key] = devices[key] return ret_devices From bb5484edacba9f3c8818e682f54e7dc2b470abc6 Mon Sep 17 00:00:00 2001 From: Niklas Morberg Date: Thu, 5 Apr 2018 18:06:23 +0200 Subject: [PATCH 116/136] Support color temperature in Homekit (#13658) * Add support for color temperature * Add test for color temp --- homeassistant/components/homekit/const.py | 1 + .../components/homekit/type_lights.py | 39 +++++++++++++++++-- tests/components/homekit/test_type_lights.py | 26 ++++++++++++- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 676f83bf8e8..d1c3d84b517 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -47,6 +47,7 @@ SERV_WINDOW_COVERING = 'WindowCovering' # #### Characteristics #### CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] +CHAR_COLOR_TEMPERATURE = 'ColorTemperature' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_POSITION = 'CurrentPosition' diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 45ed9405a2a..018d3cd2e74 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -2,13 +2,14 @@ import logging from homeassistant.components.light import ( - ATTR_HS_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, SUPPORT_COLOR) + ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS, + ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF from . import TYPES from .accessories import HomeAccessory, add_preload_service from .const import ( - CATEGORY_LIGHT, SERV_LIGHTBULB, + CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) _LOGGER = logging.getLogger(__name__) @@ -20,7 +21,7 @@ RGB_COLOR = 'rgb_color' class Light(HomeAccessory): """Generate a Light accessory for a light entity. - Currently supports: state, brightness, rgb_color. + Currently supports: state, brightness, color temperature, rgb_color. """ def __init__(self, hass, entity_id, name, **kwargs): @@ -31,7 +32,7 @@ class Light(HomeAccessory): self.entity_id = entity_id self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, CHAR_HUE: False, CHAR_SATURATION: False, - RGB_COLOR: False} + CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False} self._state = 0 self.chars = [] @@ -39,6 +40,8 @@ class Light(HomeAccessory): .attributes.get(ATTR_SUPPORTED_FEATURES) if self._features & SUPPORT_BRIGHTNESS: self.chars.append(CHAR_BRIGHTNESS) + if self._features & SUPPORT_COLOR_TEMP: + self.chars.append(CHAR_COLOR_TEMPERATURE) if self._features & SUPPORT_COLOR: self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) @@ -55,6 +58,18 @@ class Light(HomeAccessory): .get_characteristic(CHAR_BRIGHTNESS) self.char_brightness.setter_callback = self.set_brightness self.char_brightness.value = 0 + if CHAR_COLOR_TEMPERATURE in self.chars: + self.char_color_temperature = serv_light \ + .get_characteristic(CHAR_COLOR_TEMPERATURE) + self.char_color_temperature.setter_callback = \ + self.set_color_temperature + min_mireds = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MIN_MIREDS, 153) + max_mireds = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MAX_MIREDS, 500) + self.char_color_temperature.override_properties({ + 'minValue': min_mireds, 'maxValue': max_mireds}) + self.char_color_temperature.value = min_mireds if CHAR_HUE in self.chars: self.char_hue = serv_light.get_characteristic(CHAR_HUE) self.char_hue.setter_callback = self.set_hue @@ -90,6 +105,13 @@ class Light(HomeAccessory): else: self.hass.components.light.turn_off(self.entity_id) + def set_color_temperature(self, value): + """Set color temperature if call came from HomeKit.""" + _LOGGER.debug('%s: Set color temp to %s', self.entity_id, value) + self._flag[CHAR_COLOR_TEMPERATURE] = True + self.char_color_temperature.set_value(value, should_callback=False) + self.hass.components.light.turn_on(self.entity_id, color_temp=value) + def set_saturation(self, value): """Set saturation if call came from HomeKit.""" _LOGGER.debug('%s: Set saturation to %d', self.entity_id, value) @@ -141,6 +163,15 @@ class Light(HomeAccessory): should_callback=False) self._flag[CHAR_BRIGHTNESS] = False + # Handle color temperature + if CHAR_COLOR_TEMPERATURE in self.chars: + color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP) + if not self._flag[CHAR_COLOR_TEMPERATURE] \ + and isinstance(color_temperature, int): + self.char_color_temperature.set_value(color_temperature, + should_callback=False) + self._flag[CHAR_COLOR_TEMPERATURE] = False + # Handle Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: hue, saturation = new_state.attributes.get( diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index ee1900fd7c5..1cfb926c4ce 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -4,8 +4,8 @@ import unittest from homeassistant.core import callback from homeassistant.components.homekit.type_lights import Light from homeassistant.components.light import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR) + DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, + ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, EVENT_CALL_SERVICE, SERVICE_TURN_ON, @@ -118,6 +118,28 @@ class TestHomekitLights(unittest.TestCase): self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF) + def test_light_color_temperature(self): + """Test light with color temperature.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, + ATTR_COLOR_TEMP: 190}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.char_color_temperature.value, 153) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_color_temperature.value, 190) + + # Set from HomeKit + acc.char_color_temperature.set_value(250) + self.hass.block_till_done() + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 250}) + def test_light_rgb_color(self): """Test light with rgb_color.""" entity_id = 'light.demo' From 0081764ddc05f6146de45db091bd5202c3946499 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 5 Apr 2018 17:07:42 +0100 Subject: [PATCH 117/136] Remove unused CONF_WATCHERS (#13678) `CONF_WATCHERS` was from an earlier version, now unused --- homeassistant/components/folder_watcher.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/folder_watcher.py b/homeassistant/components/folder_watcher.py index 011ae892bc5..44110647632 100644 --- a/homeassistant/components/folder_watcher.py +++ b/homeassistant/components/folder_watcher.py @@ -16,7 +16,6 @@ _LOGGER = logging.getLogger(__name__) CONF_FOLDER = 'folder' CONF_PATTERNS = 'patterns' -CONF_WATCHERS = 'watchers' DEFAULT_PATTERN = '*' DOMAIN = "folder_watcher" From edcb242b6d9f90c711b42922121592c5885960eb Mon Sep 17 00:00:00 2001 From: tadly Date: Thu, 5 Apr 2018 18:44:38 +0200 Subject: [PATCH 118/136] Add media type separation for video/movie (#13612) * added media type separation for video/movie * updated all media_player components to reflect new media type --- homeassistant/components/media_player/__init__.py | 3 ++- homeassistant/components/media_player/cast.py | 4 ++-- homeassistant/components/media_player/channels.py | 4 ++-- homeassistant/components/media_player/demo.py | 4 ++-- homeassistant/components/media_player/directv.py | 4 ++-- homeassistant/components/media_player/emby.py | 4 ++-- homeassistant/components/media_player/kodi.py | 6 +++--- homeassistant/components/media_player/plex.py | 6 +++--- homeassistant/components/media_player/roku.py | 4 ++-- 9 files changed, 20 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 37536bf5586..615c758cd1a 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -83,7 +83,8 @@ ATTR_MEDIA_SHUFFLE = 'shuffle' MEDIA_TYPE_MUSIC = 'music' MEDIA_TYPE_TVSHOW = 'tvshow' -MEDIA_TYPE_VIDEO = 'movie' +MEDIA_TYPE_MOVIE = 'movie' +MEDIA_TYPE_VIDEO = 'video' MEDIA_TYPE_EPISODE = 'episode' MEDIA_TYPE_CHANNEL = 'channel' MEDIA_TYPE_PLAYLIST = 'playlist' diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 91b8d362c43..2edda0645b0 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -18,7 +18,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import (dispatcher_send, async_dispatcher_connect) from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, + MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) @@ -517,7 +517,7 @@ class CastDevice(MediaPlayerDevice): elif self.media_status.media_is_tvshow: return MEDIA_TYPE_TVSHOW elif self.media_status.media_is_movie: - return MEDIA_TYPE_VIDEO + return MEDIA_TYPE_MOVIE elif self.media_status.media_is_musictrack: return MEDIA_TYPE_MUSIC return None diff --git a/homeassistant/components/media_player/channels.py b/homeassistant/components/media_player/channels.py index 480e5152c8e..6b41ace6ce2 100644 --- a/homeassistant/components/media_player/channels.py +++ b/homeassistant/components/media_player/channels.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.media_player import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_EPISODE, - MEDIA_TYPE_VIDEO, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP, + MEDIA_TYPE_MOVIE, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, DOMAIN, PLATFORM_SCHEMA, MediaPlayerDevice) @@ -281,7 +281,7 @@ class ChannelsPlayer(MediaPlayerDevice): if media_type == MEDIA_TYPE_CHANNEL: response = self.client.play_channel(media_id) self.update_state(response) - elif media_type in [MEDIA_TYPE_VIDEO, MEDIA_TYPE_EPISODE, + elif media_type in [MEDIA_TYPE_MOVIE, MEDIA_TYPE_EPISODE, MEDIA_TYPE_TVSHOW]: response = self.client.play_recording(media_id) self.update_state(response) diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 2be7ad431cf..22fe1d005f7 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, + MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_PLAY, @@ -147,7 +147,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer): @property def media_content_type(self): """Return the content type of current playing media.""" - return MEDIA_TYPE_VIDEO + return MEDIA_TYPE_MOVIE @property def media_duration(self): diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index fae18f03cde..25d13e3017a 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -8,7 +8,7 @@ import voluptuous as vol import requests from homeassistant.components.media_player import ( - MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, + MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, MediaPlayerDevice) @@ -154,7 +154,7 @@ class DirecTvDevice(MediaPlayerDevice): """Return the content type of current playing media.""" if 'episodeTitle' in self._current: return MEDIA_TYPE_TVSHOW - return MEDIA_TYPE_VIDEO + return MEDIA_TYPE_MOVIE @property def media_channel(self): diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index 7b5658c56d9..4f9a4019268 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, + MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA) from homeassistant.const import ( @@ -231,7 +231,7 @@ class EmbyDevice(MediaPlayerDevice): if media_type == 'Episode': return MEDIA_TYPE_TVSHOW elif media_type == 'Movie': - return MEDIA_TYPE_VIDEO + return MEDIA_TYPE_MOVIE elif media_type == 'Trailer': return MEDIA_TYPE_TRAILER elif media_type == 'Music': diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 33116258978..9f2a653b8ee 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -19,8 +19,8 @@ from homeassistant.components.media_player import ( SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_PLAYLIST, - MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON) + MEDIA_TYPE_MOVIE, MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON) from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, CONF_PORT, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD, @@ -67,7 +67,7 @@ MEDIA_TYPES = { 'video': MEDIA_TYPE_VIDEO, 'set': MEDIA_TYPE_PLAYLIST, 'musicvideo': MEDIA_TYPE_VIDEO, - 'movie': MEDIA_TYPE_VIDEO, + 'movie': MEDIA_TYPE_MOVIE, 'tvshow': MEDIA_TYPE_TVSHOW, 'season': MEDIA_TYPE_TVSHOW, 'episode': MEDIA_TYPE_TVSHOW, diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index edb8aa147fb..6690382846f 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, PLATFORM_SCHEMA, + MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) @@ -480,7 +480,7 @@ class PlexClient(MediaPlayerDevice): self._media_episode = str(self._session.index).zfill(2) elif self._session_type == 'movie': - self._media_content_type = MEDIA_TYPE_VIDEO + self._media_content_type = MEDIA_TYPE_MOVIE if self._session.year is not None and \ self._media_title is not None: self._media_title += ' (' + str(self._session.year) + ')' @@ -576,7 +576,7 @@ class PlexClient(MediaPlayerDevice): elif self._session_type == 'episode': return MEDIA_TYPE_TVSHOW elif self._session_type == 'movie': - return MEDIA_TYPE_VIDEO + return MEDIA_TYPE_MOVIE elif self._session_type == 'track': return MEDIA_TYPE_MUSIC diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 15b16eec11b..87129f30db5 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA, + MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( @@ -155,7 +155,7 @@ class RokuDevice(MediaPlayerDevice): return None elif self.current_app.name == "Roku": return None - return MEDIA_TYPE_VIDEO + return MEDIA_TYPE_MOVIE @property def media_image_url(self): From 4008bf5611406325ecea7765292d4847a54fd634 Mon Sep 17 00:00:00 2001 From: PlanetJ Date: Thu, 5 Apr 2018 12:45:09 -0400 Subject: [PATCH 119/136] Adding configration to disable ip address as a requirement Fixes: #13399 (#13692) * Adding configration to disable ip address as a requirement Fixes: #13399 * Remove whitespace --- .../components/device_tracker/asuswrt.py | 5 +++- .../components/device_tracker/test_asuswrt.py | 26 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 5b8e173aba4..7e9b10e9241 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__) CONF_PUB_KEY = 'pub_key' CONF_SSH_KEY = 'ssh_key' +CONF_REQUIRE_IP = 'require_ip' DEFAULT_SSH_PORT = 22 SECRET_GROUP = 'Password or SSH Key' @@ -36,6 +37,7 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']), vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']), vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port, + vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean, vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile @@ -115,6 +117,7 @@ class AsusWrtDeviceScanner(DeviceScanner): self.protocol = config[CONF_PROTOCOL] self.mode = config[CONF_MODE] self.port = config[CONF_PORT] + self.require_ip = config[CONF_REQUIRE_IP] if self.protocol == 'ssh': self.connection = SshConnection( @@ -172,7 +175,7 @@ class AsusWrtDeviceScanner(DeviceScanner): ret_devices = {} for key in devices: - if self.mode == 'ap' or devices[key].ip is not None: + if not self.require_ip or devices[key].ip is not None: ret_devices[key] = devices[key] return ret_devices diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index 27f28412561..d2ae8965668 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker.asuswrt import ( CONF_PROTOCOL, CONF_MODE, CONF_PUB_KEY, DOMAIN, _ARP_REGEX, CONF_PORT, PLATFORM_SCHEMA, Device, get_scanner, AsusWrtDeviceScanner, - _parse_lines, SshConnection, TelnetConnection) + _parse_lines, SshConnection, TelnetConnection, CONF_REQUIRE_IP) from homeassistant.const import (CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST) @@ -105,6 +105,15 @@ WAKE_DEVICES_AP = { mac='08:09:10:11:12:14', ip='123.123.123.126', name=None) } +WAKE_DEVICES_NO_IP = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip='123.123.123.125', name=None), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip='123.123.123.126', name=None), + '08:09:10:11:12:15': Device( + mac='08:09:10:11:12:15', ip=None, name=None) +} + def setup_module(): """Setup the test module.""" @@ -411,6 +420,21 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): scanner._get_leases.return_value = LEASES_DEVICES self.assertEqual(WAKE_DEVICES_AP, scanner.get_asuswrt_data()) + def test_get_asuswrt_data_no_ip(self): + """Test for get asuswrt_data and not requiring ip.""" + conf = VALID_CONFIG_ROUTER_SSH.copy()[DOMAIN] + conf[CONF_REQUIRE_IP] = False + scanner = AsusWrtDeviceScanner(conf) + scanner._get_wl = mock.Mock() + scanner._get_arp = mock.Mock() + scanner._get_neigh = mock.Mock() + scanner._get_leases = mock.Mock() + scanner._get_wl.return_value = WL_DEVICES + scanner._get_arp.return_value = ARP_DEVICES + scanner._get_neigh.return_value = NEIGH_DEVICES + scanner._get_leases.return_value = LEASES_DEVICES + self.assertEqual(WAKE_DEVICES_NO_IP, scanner.get_asuswrt_data()) + def test_update_info(self): """Test for update info.""" scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) From e6006e9bebbcd438ca6deb17bf52e2f332ecdfe3 Mon Sep 17 00:00:00 2001 From: ikucuze <37959812+ikucuze@users.noreply.github.com> Date: Thu, 5 Apr 2018 18:56:09 +0200 Subject: [PATCH 120/136] Tahoma switches (#13636) * Added the ability to switch Tahoma Garage door relay. Those are special switches that can only be pushed. Their state is always OFF, they react to the turn_on action, perform it, but stay OFF * fixing indents and so on * CI fix --- homeassistant/components/switch/tahoma.py | 51 +++++++++++++++++++++++ homeassistant/components/tahoma.py | 3 +- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/switch/tahoma.py diff --git a/homeassistant/components/switch/tahoma.py b/homeassistant/components/switch/tahoma.py new file mode 100644 index 00000000000..339a0c39386 --- /dev/null +++ b/homeassistant/components/switch/tahoma.py @@ -0,0 +1,51 @@ +""" +Support for Tahoma Switch - those are push buttons for garage door etc. + +Those buttons are implemented as switchs that are never on. They only +receive the turn_on action, perform the relay click, and stay in OFF state + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.tahoma/ +""" +import logging + +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.tahoma import ( + DOMAIN as TAHOMA_DOMAIN, TahomaDevice) + +DEPENDENCIES = ['tahoma'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Tahoma switchs.""" + controller = hass.data[TAHOMA_DOMAIN]['controller'] + devices = [] + for switch in hass.data[TAHOMA_DOMAIN]['devices']['switch']: + devices.append(TahomaSwitch(switch, controller)) + add_devices(devices, True) + + +class TahomaSwitch(TahomaDevice, SwitchDevice): + """Representation a Tahoma Switch.""" + + @property + def device_class(self): + """Return the class of the device.""" + if self.tahoma_device.type == 'rts:GarageDoor4TRTSComponent': + return 'garage' + return None + + def turn_on(self, **kwargs): + """Send the on command.""" + self.toggle() + + def toggle(self, **kwargs): + """Click the switch.""" + self.apply_action('cycle') + + @property + def is_on(self): + """Get whether the switch is in on state.""" + return False diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 7c8d047fbcf..055e3f410ea 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) TAHOMA_COMPONENTS = [ - 'scene', 'sensor', 'cover' + 'scene', 'sensor', 'cover', 'switch' ] TAHOMA_TYPES = { @@ -43,6 +43,7 @@ TAHOMA_TYPES = { 'io:RollerShutterGenericIOComponent': 'cover', 'io:WindowOpenerVeluxIOComponent': 'cover', 'io:LightIOSystemSensor': 'sensor', + 'rts:GarageDoor4TRTSComponent': 'switch', } From 1a9727c75ac24e750105de3117e39374b0cf0319 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Thu, 5 Apr 2018 20:17:18 -0400 Subject: [PATCH 121/136] Send XY color for non-osram hue bulbs (#13665) * Send XY color for Philips hue bulbs * Review fixes * Comment tweaks --- homeassistant/components/light/hue.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 4a54f0a337d..1701b886b68 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -300,8 +300,14 @@ class HueLight(Light): command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) if ATTR_HS_COLOR in kwargs: - command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) - command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) + if self.is_osram: + command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) + command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) + else: + # Philips hue bulb models respond differently to hue/sat + # requests, so we convert to XY first to ensure a consistent + # color. + command['xy'] = color.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) elif ATTR_COLOR_TEMP in kwargs: temp = kwargs[ATTR_COLOR_TEMP] command['ct'] = max(self.min_mireds, min(temp, self.max_mireds)) From b70b23ef8350a8c00b5f19b0b3e544f0fb374db9 Mon Sep 17 00:00:00 2001 From: Mister Wil <1091741+MisterWil@users.noreply.github.com> Date: Thu, 5 Apr 2018 21:10:07 -0700 Subject: [PATCH 122/136] Update AbodePy version to 0.12.3 (#13709) --- homeassistant/components/abode.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index fde21a265b0..08918c77f01 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['abodepy==0.12.2'] +REQUIREMENTS = ['abodepy==0.12.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9a0613211da..7cd8f60fbd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -64,7 +64,7 @@ WazeRouteCalculator==0.5 YesssSMS==0.1.1b3 # homeassistant.components.abode -abodepy==0.12.2 +abodepy==0.12.3 # homeassistant.components.media_player.frontier_silicon afsapi==0.0.3 From 703eea0c93b6017d736c4254080f51624a0e93f5 Mon Sep 17 00:00:00 2001 From: jmtatsch Date: Fri, 6 Apr 2018 06:11:38 +0200 Subject: [PATCH 123/136] Enable autodiscovery for mqtt cameras (#13697) * Enable autodiscovery for mqtt cameras, BREAKING CHANGE: homogenisation topic->state_topic * fix line too long * fix topic->state_topic in test * image shall not be the state of entity --- homeassistant/components/camera/mqtt.py | 9 ++++++--- homeassistant/components/mqtt/discovery.py | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/camera/mqtt.py b/homeassistant/components/camera/mqtt.py index b7a7510e0eb..b2a27230a02 100644 --- a/homeassistant/components/camera/mqtt.py +++ b/homeassistant/components/camera/mqtt.py @@ -19,7 +19,6 @@ from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_TOPIC = 'topic' - DEFAULT_NAME = 'MQTT Camera' DEPENDENCIES = ['mqtt'] @@ -33,9 +32,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the MQTT Camera.""" - topic = config[CONF_TOPIC] + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) - async_add_devices([MqttCamera(config[CONF_NAME], topic)]) + async_add_devices([MqttCamera( + config.get(CONF_NAME), + config.get(CONF_TOPIC) + )]) class MqttCamera(Camera): diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 3263521f3f1..d5a3b4a2efb 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -20,10 +20,12 @@ TOPIC_MATCHER = re.compile( r'(?:(?P[a-zA-Z0-9_-]+)/)?(?P[a-zA-Z0-9_-]+)/config') SUPPORTED_COMPONENTS = [ - 'binary_sensor', 'cover', 'fan', 'light', 'sensor', 'switch', 'lock'] + 'binary_sensor', 'camera', 'cover', 'fan', + 'light', 'sensor', 'switch', 'lock'] ALLOWED_PLATFORMS = { 'binary_sensor': ['mqtt'], + 'camera': ['mqtt'], 'cover': ['mqtt'], 'fan': ['mqtt'], 'light': ['mqtt', 'mqtt_json', 'mqtt_template'], From 1d7ecc22f91e2ad037ff350084760ff282f93e9e Mon Sep 17 00:00:00 2001 From: Timmo <28114703+timmo001@users.noreply.github.com> Date: Fri, 6 Apr 2018 09:59:09 +0100 Subject: [PATCH 124/136] Added ENTITY_ID_FORMAT import and set entity_id in __init__ (#13642) --- homeassistant/components/switch/broadlink.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 3828758fe6e..3e620a6a25b 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -14,7 +14,7 @@ import socket import voluptuous as vol from homeassistant.components.switch import ( - DOMAIN, PLATFORM_SCHEMA, SwitchDevice) + DOMAIN, PLATFORM_SCHEMA, SwitchDevice, ENTITY_ID_FORMAT) from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC, CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE) @@ -150,6 +150,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for object_id, device_config in devices.items(): switches.append( BroadlinkRMSwitch( + object_id, device_config.get(CONF_FRIENDLY_NAME, object_id), broadlink_device, device_config.get(CONF_COMMAND_ON), @@ -184,8 +185,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class BroadlinkRMSwitch(SwitchDevice): """Representation of an Broadlink switch.""" - def __init__(self, friendly_name, device, command_on, command_off): + def __init__(self, name, friendly_name, device, command_on, command_off): """Initialize the switch.""" + self.entity_id = ENTITY_ID_FORMAT.format(name) self._name = friendly_name self._state = False self._command_on = b64decode(command_on) if command_on else None From bddfe24753524ffb812a8402f9522a781ed997ea Mon Sep 17 00:00:00 2001 From: Philipp Schmitt Date: Fri, 6 Apr 2018 11:21:05 +0200 Subject: [PATCH 125/136] Fix #10175 (#13713) --- homeassistant/components/media_player/liveboxplaytv.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/liveboxplaytv.py b/homeassistant/components/media_player/liveboxplaytv.py index 8093f0d3dbe..4fe4da5a942 100644 --- a/homeassistant/components/media_player/liveboxplaytv.py +++ b/homeassistant/components/media_player/liveboxplaytv.py @@ -22,7 +22,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['liveboxplaytv==2.0.2', 'pyteleloisirs==3.3'] +REQUIREMENTS = ['liveboxplaytv==2.0.2', 'pyteleloisirs==3.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7cd8f60fbd6..c29b78c8ded 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -917,7 +917,7 @@ pystride==0.1.7 pysyncthru==0.3.1 # homeassistant.components.media_player.liveboxplaytv -pyteleloisirs==3.3 +pyteleloisirs==3.4 # homeassistant.components.sensor.thinkingcleaner # homeassistant.components.switch.thinkingcleaner From 0a25d30ba69d836289dee429b329c925fddc351a Mon Sep 17 00:00:00 2001 From: Marco Orovecchia Date: Fri, 6 Apr 2018 15:34:56 +0200 Subject: [PATCH 126/136] Add support for Nanoleaf Aurora Light Panels (#13456) * Added support for Nanoleaf Aurora Light Panels * aurora light module - fixed lint errors * aurora light module - use SUPPORT_COLOR instead of SUPPORT_RGB_COLOR * nanoleaf aurora light - support_hs_color instead of rgb * review comments from armills implemented * nanoleaf aurora lights - put attributes into constructor (pylint) --- .coveragerc | 1 + homeassistant/components/light/aurora.py | 153 +++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 157 insertions(+) create mode 100644 homeassistant/components/light/aurora.py diff --git a/.coveragerc b/.coveragerc index 79b1cffa6fe..a84a739151c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -406,6 +406,7 @@ omit = homeassistant/components/image_processing/seven_segments.py homeassistant/components/keyboard_remote.py homeassistant/components/keyboard.py + homeassistant/components/light/aurora.py homeassistant/components/light/avion.py homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinkt.py diff --git a/homeassistant/components/light/aurora.py b/homeassistant/components/light/aurora.py new file mode 100644 index 00000000000..2a9066bd55f --- /dev/null +++ b/homeassistant/components/light/aurora.py @@ -0,0 +1,153 @@ +""" +Support for Nanoleaf Aurora platform. + +Based in large parts upon Software-2's ha-aurora and fully +reliant on Software-2's nanoleaf-aurora Python Library, see +https://github.com/software-2/ha-aurora as well as +https://github.com/software-2/nanoleaf + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.nanoleaf_aurora/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + SUPPORT_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, + SUPPORT_COLOR, PLATFORM_SCHEMA, Light) +from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.util import color as color_util +from homeassistant.util.color import \ + color_temperature_mired_to_kelvin as mired_to_kelvin + +REQUIREMENTS = ['nanoleaf==0.4.1'] + +SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | + SUPPORT_COLOR) + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_NAME, default='Aurora'): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup Nanoleaf Aurora device.""" + import nanoleaf + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + token = config.get(CONF_TOKEN) + aurora_light = nanoleaf.Aurora(host, token) + aurora_light.hass_name = name + + if aurora_light.on is None: + _LOGGER.error("Could not connect to \ + Nanoleaf Aurora: %s on %s", name, host) + add_devices([AuroraLight(aurora_light)], True) + + +class AuroraLight(Light): + """Representation of a Nanoleaf Aurora.""" + + def __init__(self, light): + """Initialize an Aurora.""" + self._brightness = None + self._color_temp = None + self._effect = None + self._effects_list = None + self._light = light + self._name = light.hass_name + self._hs_color = None + self._state = None + + @property + def brightness(self): + """Return the brightness of the light.""" + if self._brightness is not None: + return int(self._brightness * 2.55) + return None + + @property + def color_temp(self): + """Return the current color temperature.""" + if self._color_temp is not None: + return color_util.color_temperature_kelvin_to_mired( + self._color_temp) + return None + + @property + def effect(self): + """Return the current effect.""" + return self._effect + + @property + def effect_list(self): + """Return the list of supported effects.""" + return self._effects_list + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:triangle-outline" + + @property + def is_on(self): + """Return true if light is on.""" + return self._state + + @property + def hs_color(self): + """Return the color in HS.""" + return self._hs_color + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_AURORA + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + self._light.on = True + brightness = kwargs.get(ATTR_BRIGHTNESS) + hs_color = kwargs.get(ATTR_HS_COLOR) + color_temp_mired = kwargs.get(ATTR_COLOR_TEMP) + effect = kwargs.get(ATTR_EFFECT) + + if hs_color: + hue, saturation = hs_color + self._light.hue = int(hue) + self._light.saturation = int(saturation) + + if color_temp_mired: + self._light.color_temperature = mired_to_kelvin(color_temp_mired) + if brightness: + self._light.brightness = int(brightness / 2.55) + if effect: + self._light.effect = effect + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._light.on = False + + def update(self): + """Fetch new state data for this light. + + This is the only method that should fetch new data for Home Assistant. + """ + self._brightness = self._light.brightness + self._color_temp = self._light.color_temperature + self._effect = self._light.effect + self._effects_list = self._light.effects_list + self._hs_color = self._light.hue, self._light.saturation + self._state = self._light.on diff --git a/requirements_all.txt b/requirements_all.txt index c29b78c8ded..d2c56e74d97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -536,6 +536,9 @@ myusps==1.3.2 # homeassistant.components.media_player.nadtcp nad_receiver==0.0.9 +# homeassistant.components.light.aurora +nanoleaf==0.4.1 + # homeassistant.components.discovery netdisco==1.3.0 From bd51143ac193780b8d08d5058b452c8d53b60c33 Mon Sep 17 00:00:00 2001 From: David Broadfoot Date: Fri, 6 Apr 2018 23:53:00 +1000 Subject: [PATCH 127/136] Added gogogate2 cover (#13467) * Added gogogate2 cover * Hound fixes * PR feedback * Hound comments * Bump gogogate2 version * Update requirements all * Add device_class and features * Fix lint issues * Again lint * Fix imports * Fix end of file --- .coveragerc | 1 + homeassistant/components/cover/gogogate2.py | 120 ++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 124 insertions(+) create mode 100644 homeassistant/components/cover/gogogate2.py diff --git a/.coveragerc b/.coveragerc index a84a739151c..e9c69d137e2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -352,6 +352,7 @@ omit = homeassistant/components/climate/touchline.py homeassistant/components/climate/venstar.py homeassistant/components/cover/garadget.py + homeassistant/components/cover/gogogate2.py homeassistant/components/cover/homematic.py homeassistant/components/cover/knx.py homeassistant/components/cover/myq.py diff --git a/homeassistant/components/cover/gogogate2.py b/homeassistant/components/cover/gogogate2.py new file mode 100644 index 00000000000..c2bdc9c5472 --- /dev/null +++ b/homeassistant/components/cover/gogogate2.py @@ -0,0 +1,120 @@ +""" +Support for Gogogate2 Garage Doors. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/cover.gogogate2/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.cover import ( + CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE) +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, STATE_UNKNOWN, + CONF_IP_ADDRESS, CONF_NAME) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pygogogate2==0.0.3'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'gogogate2' + +NOTIFICATION_ID = 'gogogate2_notification' +NOTIFICATION_TITLE = 'Gogogate2 Cover Setup' + +COVER_SCHEMA = vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Gogogate2 component.""" + from pygogogate2 import Gogogate2API as pygogogate2 + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + ip_address = config.get(CONF_IP_ADDRESS) + name = config.get(CONF_NAME) + mygogogate2 = pygogogate2(username, password, ip_address) + + try: + devices = mygogogate2.get_devices() + if devices is False: + raise ValueError( + "Username or Password is incorrect or no devices found") + + add_devices(MyGogogate2Device( + mygogogate2, door, name) for door in devices) + return + + except (TypeError, KeyError, NameError, ValueError) as ex: + _LOGGER.error("%s", ex) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return + + +class MyGogogate2Device(CoverDevice): + """Representation of a Gogogate2 cover.""" + + def __init__(self, mygogogate2, device, name): + """Initialize with API object, device id.""" + self.mygogogate2 = mygogogate2 + self.device_id = device['door'] + self._name = name or device['name'] + self._status = device['status'] + self.available = None + + @property + def name(self): + """Return the name of the garage door if any.""" + return self._name if self._name else DEFAULT_NAME + + @property + def is_closed(self): + """Return true if cover is closed, else False.""" + return self._status == STATE_CLOSED + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'garage' + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE + + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self.available + + def close_cover(self, **kwargs): + """Issue close command to cover.""" + self.mygogogate2.close_device(self.device_id) + self.schedule_update_ha_state(True) + + def open_cover(self, **kwargs): + """Issue open command to cover.""" + self.mygogogate2.open_device(self.device_id) + self.schedule_update_ha_state(True) + + def update(self): + """Update status of cover.""" + try: + self._status = self.mygogogate2.get_status(self.device_id) + self.available = True + except (TypeError, KeyError, NameError, ValueError) as ex: + _LOGGER.error("%s", ex) + self._status = STATE_UNKNOWN + self.available = False diff --git a/requirements_all.txt b/requirements_all.txt index d2c56e74d97..da2373443cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -764,6 +764,9 @@ pyflexit==0.3 # homeassistant.components.ifttt pyfttt==0.3 +# homeassistant.components.cover.gogogate2 +pygogogate2==0.0.3 + # homeassistant.components.remote.harmony pyharmony==1.0.20 From 9ae6a3402c44ad8466a9ff9b75a611672615be0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 6 Apr 2018 10:26:08 -0400 Subject: [PATCH 128/136] Version bump to 0.67.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d286aa85458..815562b68c5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 67 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From bd58a0de7dab6dc8119ddea5878f27ed1fb25c1e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Apr 2018 21:21:26 -0400 Subject: [PATCH 129/136] Remove vendor lookup for mac addresses (#13788) * Remove vendor lookup for mac addresses * Fix tests --- .../components/device_tracker/__init__.py | 61 +-------- tests/components/device_tracker/test_init.py | 128 +----------------- tests/conftest.py | 4 +- 3 files changed, 6 insertions(+), 187 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 682496335a0..45f0e51a214 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -9,8 +9,6 @@ from datetime import timedelta import logging from typing import Any, List, Sequence, Callable -import aiohttp -import async_timeout import voluptuous as vol from homeassistant.setup import async_prepare_setup_platform @@ -19,7 +17,6 @@ from homeassistant.loader import bind_hass from homeassistant.components import group, zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval @@ -76,7 +73,6 @@ ATTR_LOCATION_NAME = 'location_name' ATTR_MAC = 'mac' ATTR_NAME = 'name' ATTR_SOURCE_TYPE = 'source_type' -ATTR_VENDOR = 'vendor' ATTR_CONSIDER_HOME = 'consider_home' SOURCE_TYPE_GPS = 'gps' @@ -328,14 +324,10 @@ class DeviceTracker(object): self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id]) - # lookup mac vendor string to be stored in config - yield from device.set_vendor_for_mac() - self.hass.bus.async_fire(EVENT_NEW_DEVICE, { ATTR_ENTITY_ID: device.entity_id, ATTR_HOST_NAME: device.host_name, ATTR_MAC: device.mac, - ATTR_VENDOR: device.vendor, }) # update known_devices.yaml @@ -413,7 +405,6 @@ class Device(Entity): consider_home = None # type: dt_util.dt.timedelta battery = None # type: int attributes = None # type: dict - vendor = None # type: str icon = None # type: str # Track if the last update of this device was HOME. @@ -423,7 +414,7 @@ class Device(Entity): def __init__(self, hass: HomeAssistantType, consider_home: timedelta, track: bool, dev_id: str, mac: str, name: str = None, picture: str = None, gravatar: str = None, icon: str = None, - hide_if_away: bool = False, vendor: str = None) -> None: + hide_if_away: bool = False) -> None: """Initialize a device.""" self.hass = hass self.entity_id = ENTITY_ID_FORMAT.format(dev_id) @@ -451,7 +442,6 @@ class Device(Entity): self.icon = icon self.away_hide = hide_if_away - self.vendor = vendor self.source_type = None @@ -567,51 +557,6 @@ class Device(Entity): self._state = STATE_HOME self.last_update_home = True - @asyncio.coroutine - def set_vendor_for_mac(self): - """Set vendor string using api.macvendors.com.""" - self.vendor = yield from self.get_vendor_for_mac() - - @asyncio.coroutine - def get_vendor_for_mac(self): - """Try to find the vendor string for a given MAC address.""" - if not self.mac: - return None - - if '_' in self.mac: - _, mac = self.mac.split('_', 1) - else: - mac = self.mac - - if not len(mac.split(':')) == 6: - return 'unknown' - - # We only need the first 3 bytes of the MAC for a lookup - # this improves somewhat on privacy - oui_bytes = mac.split(':')[0:3] - # bytes like 00 get truncates to 0, API needs full bytes - oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes]) - url = 'http://api.macvendors.com/' + oui - try: - websession = async_get_clientsession(self.hass) - - with async_timeout.timeout(5, loop=self.hass.loop): - resp = yield from websession.get(url) - # mac vendor found, response is the string - if resp.status == 200: - vendor_string = yield from resp.text() - return vendor_string - # If vendor is not known to the API (404) or there - # was a failure during the lookup (500); set vendor - # to something other then None to prevent retry - # as the value is only relevant when it is to be stored - # in the 'known_devices.yaml' file which only happens - # the first time the device is seen. - return 'unknown' - except (asyncio.TimeoutError, aiohttp.ClientError): - # Same as above - return 'unknown' - @asyncio.coroutine def async_added_to_hass(self): """Add an entity.""" @@ -685,7 +630,6 @@ def async_load_config(path: str, hass: HomeAssistantType, vol.Optional('picture', default=None): vol.Any(None, cv.string), vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( cv.time_period, cv.positive_timedelta), - vol.Optional('vendor', default=None): vol.Any(None, cv.string), }) try: result = [] @@ -697,6 +641,8 @@ def async_load_config(path: str, hass: HomeAssistantType, return [] for dev_id, device in devices.items(): + # Deprecated option. We just ignore it to avoid breaking change + device.pop('vendor', None) try: device = dev_schema(device) device['dev_id'] = cv.slugify(dev_id) @@ -772,7 +718,6 @@ def update_config(path: str, dev_id: str, device: Device): 'picture': device.config_picture, 'track': device.track, CONF_AWAY_HIDE: device.away_hide, - 'vendor': device.vendor, }} out.write('\n') out.write(dump(device)) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index c051983d8fa..912bd315ecd 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -24,9 +24,7 @@ from homeassistant.remote import JSONEncoder from tests.common import ( get_test_home_assistant, fire_time_changed, - patch_yaml_files, assert_setup_component, mock_restore_cache, mock_coro) - -from ...test_util.aiohttp import mock_aiohttp_client + patch_yaml_files, assert_setup_component, mock_restore_cache) TEST_PLATFORM = {device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}} @@ -111,7 +109,6 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertEqual(device.config_picture, config.config_picture) self.assertEqual(device.away_hide, config.away_hide) self.assertEqual(device.consider_home, config.consider_home) - self.assertEqual(device.vendor, config.vendor) self.assertEqual(device.icon, config.icon) # pylint: disable=invalid-name @@ -173,124 +170,6 @@ class TestComponentsDeviceTracker(unittest.TestCase): "55502f40dc8b7c769880b10874abc9d0.jpg?s=80&d=wavatar") self.assertEqual(device.config_picture, gravatar_url) - def test_mac_vendor_lookup(self): - """Test if vendor string is lookup on macvendors API.""" - mac = 'B8:27:EB:00:00:00' - vendor_string = 'Raspberry Pi Foundation' - - device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - text=vendor_string) - - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - assert aioclient_mock.call_count == 1 - - self.assertEqual(device.vendor, vendor_string) - - def test_mac_vendor_mac_formats(self): - """Verify all variations of MAC addresses are handled correctly.""" - vendor_string = 'Raspberry Pi Foundation' - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - text=vendor_string) - aioclient_mock.get('http://api.macvendors.com/00:27:eb', - text=vendor_string) - - mac = 'B8:27:EB:00:00:00' - device = device_tracker.Device( - self.hass, timedelta(seconds=180), - True, 'test', mac, 'Test name') - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - self.assertEqual(device.vendor, vendor_string) - - mac = '0:27:EB:00:00:00' - device = device_tracker.Device( - self.hass, timedelta(seconds=180), - True, 'test', mac, 'Test name') - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - self.assertEqual(device.vendor, vendor_string) - - mac = 'PREFIXED_B8:27:EB:00:00:00' - device = device_tracker.Device( - self.hass, timedelta(seconds=180), - True, 'test', mac, 'Test name') - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - self.assertEqual(device.vendor, vendor_string) - - def test_mac_vendor_lookup_unknown(self): - """Prevent another mac vendor lookup if was not found first time.""" - mac = 'B8:27:EB:00:00:00' - - device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - status=404) - - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - - self.assertEqual(device.vendor, 'unknown') - - def test_mac_vendor_lookup_error(self): - """Prevent another lookup if failure during API call.""" - mac = 'B8:27:EB:00:00:00' - - device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - status=500) - - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - - self.assertEqual(device.vendor, 'unknown') - - def test_mac_vendor_lookup_exception(self): - """Prevent another lookup if exception during API call.""" - mac = 'B8:27:EB:00:00:00' - - device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - exc=asyncio.TimeoutError()) - - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - - self.assertEqual(device.vendor, 'unknown') - - def test_mac_vendor_lookup_on_see(self): - """Test if macvendor is looked up when device is seen.""" - mac = 'B8:27:EB:00:00:00' - vendor_string = 'Raspberry Pi Foundation' - - tracker = device_tracker.DeviceTracker( - self.hass, timedelta(seconds=60), 0, {}, []) - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - text=vendor_string) - - run_coroutine_threadsafe( - tracker.async_see(mac=mac), self.hass.loop).result() - assert aioclient_mock.call_count == 1, \ - 'No http request for macvendor made!' - self.assertEqual(tracker.devices['b827eb000000'].vendor, vendor_string) - @patch( 'homeassistant.components.device_tracker.DeviceTracker.see') @patch( @@ -463,7 +342,6 @@ class TestComponentsDeviceTracker(unittest.TestCase): 'entity_id': 'device_tracker.hello', 'host_name': 'hello', 'mac': 'MAC_1', - 'vendor': 'unknown', } # pylint: disable=invalid-name @@ -495,9 +373,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): timedelta(seconds=0)) assert len(config) == 0 - @patch('homeassistant.components.device_tracker.Device' - '.set_vendor_for_mac', return_value=mock_coro()) - def test_see_state(self, mock_set_vendor): + def test_see_state(self): """Test device tracker see records state correctly.""" self.assertTrue(setup_component(self.hass, device_tracker.DOMAIN, TEST_PLATFORM)) diff --git a/tests/conftest.py b/tests/conftest.py index 8f0ca787721..269d460ebb6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -123,7 +123,5 @@ def mock_device_tracker_conf(): ), patch( 'homeassistant.components.device_tracker.async_load_config', side_effect=lambda *args: mock_coro(devices) - ), patch('homeassistant.components.device_tracker' - '.Device.set_vendor_for_mac'): - + ): yield devices From 09dbd94467490e2d780181a70f00d709913400ee Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Tue, 10 Apr 2018 14:11:00 -0400 Subject: [PATCH 130/136] iglo hs color fix (#13808) --- homeassistant/components/light/iglo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py index 77e3972968c..f40dc2ce84e 100644 --- a/homeassistant/components/light/iglo.py +++ b/homeassistant/components/light/iglo.py @@ -79,7 +79,7 @@ class IGloLamp(Light): @property def hs_color(self): """Return the hs value.""" - return color_util.color_RGB_to_hsv(*self._lamp.state()['rgb']) + return color_util.color_RGB_to_hs(*self._lamp.state()['rgb']) @property def effect(self): From 234495ed0549cd33490ac41329fe26bf0099c645 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 12 Apr 2018 02:58:57 +0200 Subject: [PATCH 131/136] Fix too green color conversion (#13828) * Prepare test * Fix too green color conversion * Fix remaining tests --- homeassistant/util/color.py | 2 +- tests/components/light/test_demo.py | 8 ++-- tests/components/light/test_mqtt.py | 10 ++-- tests/components/light/test_mqtt_json.py | 2 +- tests/components/switch/test_flux.py | 58 ++++++++++++------------ tests/util/test_color.py | 20 ++++---- 6 files changed, 50 insertions(+), 50 deletions(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index c2e4ac737e8..32e9df70a03 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -203,7 +203,7 @@ def color_RGB_to_xy_brightness( # Wide RGB D65 conversion formula X = R * 0.664511 + G * 0.154324 + B * 0.162028 - Y = R * 0.313881 + G * 0.668433 + B * 0.047685 + Y = R * 0.283881 + G * 0.668433 + B * 0.047685 Z = R * 0.000088 + G * 0.072310 + B * 0.986039 # Convert XYZ to xy diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index 963cda6abc4..8ba6385166b 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -29,15 +29,15 @@ class TestDemoLight(unittest.TestCase): def test_state_attributes(self): """Test light state attributes.""" light.turn_on( - self.hass, ENTITY_LIGHT, xy_color=(.4, .6), brightness=25) + self.hass, ENTITY_LIGHT, xy_color=(.4, .4), brightness=25) self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) - self.assertEqual((0.378, 0.574), state.attributes.get( + self.assertEqual((0.4, 0.4), state.attributes.get( light.ATTR_XY_COLOR)) self.assertEqual(25, state.attributes.get(light.ATTR_BRIGHTNESS)) self.assertEqual( - (207, 255, 0), state.attributes.get(light.ATTR_RGB_COLOR)) + (255, 234, 164), state.attributes.get(light.ATTR_RGB_COLOR)) self.assertEqual('rainbow', state.attributes.get(light.ATTR_EFFECT)) light.turn_on( self.hass, ENTITY_LIGHT, rgb_color=(251, 253, 255), @@ -48,7 +48,7 @@ class TestDemoLight(unittest.TestCase): self.assertEqual( (250, 252, 255), state.attributes.get(light.ATTR_RGB_COLOR)) self.assertEqual( - (0.316, 0.333), state.attributes.get(light.ATTR_XY_COLOR)) + (0.319, 0.326), state.attributes.get(light.ATTR_XY_COLOR)) light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400, effect='none') self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 71fe77ef6be..7f7841b1a69 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -255,7 +255,7 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual(150, state.attributes.get('color_temp')) self.assertEqual('none', state.attributes.get('effect')) self.assertEqual(255, state.attributes.get('white_value')) - self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) + self.assertEqual((0.323, 0.329), state.attributes.get('xy_color')) fire_mqtt_message(self.hass, 'test_light_rgb/status', '0') self.hass.block_till_done() @@ -311,7 +311,7 @@ class TestLightMQTT(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual((0.652, 0.343), + self.assertEqual((0.672, 0.324), light_state.attributes.get('xy_color')) def test_brightness_controlling_scale(self): @@ -519,7 +519,7 @@ class TestLightMQTT(unittest.TestCase): mock.call('test_light_rgb/rgb/set', '50,50,50', 2, False), mock.call('test_light_rgb/brightness/set', 50, 2, False), mock.call('test_light_rgb/white_value/set', 80, 2, False), - mock.call('test_light_rgb/xy/set', '0.32,0.336', 2, False), + mock.call('test_light_rgb/xy/set', '0.323,0.329', 2, False), ], any_order=True) state = self.hass.states.get('light.test') @@ -527,7 +527,7 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual((255, 255, 255), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(80, state.attributes['white_value']) - self.assertEqual((0.32, 0.336), state.attributes['xy_color']) + self.assertEqual((0.323, 0.329), state.attributes['xy_color']) def test_sending_mqtt_rgb_command_with_template(self): """Test the sending of RGB command with template.""" @@ -679,7 +679,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) + self.assertEqual((0.323, 0.329), state.attributes.get('xy_color')) def test_on_command_first(self): """Test on command being sent before brightness.""" diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index a183355fbb3..d6835b00be0 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -206,7 +206,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(155, state.attributes.get('color_temp')) self.assertEqual('colorloop', state.attributes.get('effect')) self.assertEqual(150, state.attributes.get('white_value')) - self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) + self.assertEqual((0.323, 0.329), state.attributes.get('xy_color')) # Turn the light off fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}') diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index a1e600860f9..c42061db958 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -154,8 +154,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) # pylint: disable=invalid-name def test_flux_after_sunrise_before_sunset(self): @@ -201,8 +201,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 180) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.431, 0.38]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 173) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.439, 0.37]) # pylint: disable=invalid-name def test_flux_after_sunset_before_stop(self): @@ -249,8 +249,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 153) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.496, 0.397]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 146) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.506, 0.385]) # pylint: disable=invalid-name def test_flux_after_stop_before_sunrise(self): @@ -296,8 +296,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) # pylint: disable=invalid-name def test_flux_with_custom_start_stop_times(self): @@ -345,8 +345,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 154) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.494, 0.397]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 147) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.504, 0.385]) def test_flux_before_sunrise_stop_next_day(self): """Test the flux switch before sunrise. @@ -395,8 +395,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) # pylint: disable=invalid-name def test_flux_after_sunrise_before_sunset_stop_next_day(self): @@ -447,8 +447,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 180) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.431, 0.38]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 173) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.439, 0.37]) # pylint: disable=invalid-name def test_flux_after_sunset_before_midnight_stop_next_day(self): @@ -498,8 +498,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 126) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.574, 0.401]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.588, 0.386]) # pylint: disable=invalid-name def test_flux_after_sunset_after_midnight_stop_next_day(self): @@ -549,8 +549,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 122) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.586, 0.397]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 114) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.601, 0.382]) # pylint: disable=invalid-name def test_flux_after_stop_before_sunrise_stop_next_day(self): @@ -600,8 +600,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) # pylint: disable=invalid-name def test_flux_with_custom_colortemps(self): @@ -650,8 +650,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 167) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.461, 0.389]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 159) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.469, 0.378]) # pylint: disable=invalid-name def test_flux_with_custom_brightness(self): @@ -700,7 +700,7 @@ class TestSwitchFlux(unittest.TestCase): self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 255) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.496, 0.397]) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.506, 0.385]) def test_flux_with_multiple_lights(self): """Test the flux switch with multiple light entities.""" @@ -762,14 +762,14 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 171) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.452, 0.386]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376]) call = turn_on_calls[-2] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 171) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.452, 0.386]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376]) call = turn_on_calls[-3] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 171) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.452, 0.386]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376]) def test_flux_with_mired(self): """Test the flux switch´s mode mired.""" diff --git a/tests/util/test_color.py b/tests/util/test_color.py index b64cf0acf80..74ba72cd3d1 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -14,7 +14,7 @@ class TestColorUtil(unittest.TestCase): """Test color_RGB_to_xy_brightness.""" self.assertEqual((0, 0, 0), color_util.color_RGB_to_xy_brightness(0, 0, 0)) - self.assertEqual((0.32, 0.336, 255), + self.assertEqual((0.323, 0.329, 255), color_util.color_RGB_to_xy_brightness(255, 255, 255)) self.assertEqual((0.136, 0.04, 12), @@ -23,17 +23,17 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((0.172, 0.747, 170), color_util.color_RGB_to_xy_brightness(0, 255, 0)) - self.assertEqual((0.679, 0.321, 80), + self.assertEqual((0.701, 0.299, 72), color_util.color_RGB_to_xy_brightness(255, 0, 0)) - self.assertEqual((0.679, 0.321, 17), + self.assertEqual((0.701, 0.299, 16), color_util.color_RGB_to_xy_brightness(128, 0, 0)) def test_color_RGB_to_xy(self): """Test color_RGB_to_xy.""" self.assertEqual((0, 0), color_util.color_RGB_to_xy(0, 0, 0)) - self.assertEqual((0.32, 0.336), + self.assertEqual((0.323, 0.329), color_util.color_RGB_to_xy(255, 255, 255)) self.assertEqual((0.136, 0.04), @@ -42,10 +42,10 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((0.172, 0.747), color_util.color_RGB_to_xy(0, 255, 0)) - self.assertEqual((0.679, 0.321), + self.assertEqual((0.701, 0.299), color_util.color_RGB_to_xy(255, 0, 0)) - self.assertEqual((0.679, 0.321), + self.assertEqual((0.701, 0.299), color_util.color_RGB_to_xy(128, 0, 0)) def test_color_xy_brightness_to_RGB(self): @@ -155,16 +155,16 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((0.151, 0.343), color_util.color_hs_to_xy(180, 100)) - self.assertEqual((0.352, 0.329), + self.assertEqual((0.356, 0.321), color_util.color_hs_to_xy(350, 12.5)) - self.assertEqual((0.228, 0.476), + self.assertEqual((0.229, 0.474), color_util.color_hs_to_xy(140, 50)) - self.assertEqual((0.465, 0.33), + self.assertEqual((0.474, 0.317), color_util.color_hs_to_xy(0, 40)) - self.assertEqual((0.32, 0.336), + self.assertEqual((0.323, 0.329), color_util.color_hs_to_xy(360, 0)) def test_rgb_hex_to_rgb_list(self): From f29904f1b56c2a486111374ee81d1898e1f04fd6 Mon Sep 17 00:00:00 2001 From: Marco Orovecchia Date: Thu, 12 Apr 2018 09:24:07 +0200 Subject: [PATCH 132/136] Rename from aurora light to nanoleaf_aurora (#13831) --- .coveragerc | 2 +- .../components/light/{aurora.py => nanoleaf_aurora.py} | 0 requirements_all.txt | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename homeassistant/components/light/{aurora.py => nanoleaf_aurora.py} (100%) diff --git a/.coveragerc b/.coveragerc index e9c69d137e2..48b45db347b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -407,7 +407,6 @@ omit = homeassistant/components/image_processing/seven_segments.py homeassistant/components/keyboard_remote.py homeassistant/components/keyboard.py - homeassistant/components/light/aurora.py homeassistant/components/light/avion.py homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinkt.py @@ -422,6 +421,7 @@ omit = homeassistant/components/light/lifx.py homeassistant/components/light/limitlessled.py homeassistant/components/light/mystrom.py + homeassistant/components/light/nanoleaf_aurora.py homeassistant/components/light/osramlightify.py homeassistant/components/light/piglow.py homeassistant/components/light/rpi_gpio_pwm.py diff --git a/homeassistant/components/light/aurora.py b/homeassistant/components/light/nanoleaf_aurora.py similarity index 100% rename from homeassistant/components/light/aurora.py rename to homeassistant/components/light/nanoleaf_aurora.py diff --git a/requirements_all.txt b/requirements_all.txt index da2373443cb..8fe9c7e1c13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -536,7 +536,7 @@ myusps==1.3.2 # homeassistant.components.media_player.nadtcp nad_receiver==0.0.9 -# homeassistant.components.light.aurora +# homeassistant.components.light.nanoleaf_aurora nanoleaf==0.4.1 # homeassistant.components.discovery From 9bd29589d50a7832a63378cdcaa5674d1fe4ef2c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 12 Apr 2018 08:22:07 -0400 Subject: [PATCH 133/136] Version bump to 0.67.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 815562b68c5..53c72a46c3f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 67 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From b9306a5e521b855b6dddf4b92136a2c3c24b82e2 Mon Sep 17 00:00:00 2001 From: stephanerosi Date: Thu, 12 Apr 2018 15:44:56 +0200 Subject: [PATCH 134/136] Channel up/down for LiveTV and next/previous for other apps (#13829) --- homeassistant/components/media_player/webostv.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 860d69e22c3..ae9d259a47c 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -35,6 +35,7 @@ CONF_SOURCES = 'sources' CONF_ON_ACTION = 'turn_on_action' DEFAULT_NAME = 'LG webOS Smart TV' +LIVETV_APP_ID = 'com.webos.app.livetv' WEBOSTV_CONFIG_FILE = 'webostv.conf' @@ -357,8 +358,16 @@ class LgWebOSDevice(MediaPlayerDevice): def media_next_track(self): """Send next track command.""" - self._client.channel_up() + current_input = self._client.get_input() + if current_input == LIVETV_APP_ID: + self._client.channel_up() + else: + self._client.fast_forward() def media_previous_track(self): """Send the previous track command.""" - self._client.channel_down() + current_input = self._client.get_input() + if current_input == LIVETV_APP_ID: + self._client.channel_down() + else: + self._client.rewind() From 598f093bf0fecdefaa3d95d1ddae71317a05321e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Apr 2018 07:32:05 -0400 Subject: [PATCH 135/136] Add authentication to error log endpoint (#13836) --- homeassistant/components/api.py | 16 +++++++++++++--- tests/components/test_api.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index d272ebcb1c0..6fdf0c027a4 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -52,9 +52,8 @@ def setup(hass, config): hass.http.register_view(APIComponentsView) hass.http.register_view(APITemplateView) - log_path = hass.data.get(DATA_LOGGING, None) - if log_path: - hass.http.register_static_path(URL_API_ERROR_LOG, log_path, False) + if DATA_LOGGING in hass.data: + hass.http.register_view(APIErrorLog) return True @@ -356,6 +355,17 @@ class APITemplateView(HomeAssistantView): HTTP_BAD_REQUEST) +class APIErrorLog(HomeAssistantView): + """View to fetch the error log.""" + + url = URL_API_ERROR_LOG + name = "api:error_log" + + async def get(self, request): + """Retrieve API error log.""" + return await self.file(request, request.app['hass'].data[DATA_LOGGING]) + + @asyncio.coroutine def async_services_json(hass): """Generate services data to JSONify.""" diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 6d5bec046f1..c9dae27d14c 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -2,13 +2,18 @@ # pylint: disable=protected-access import asyncio import json +from unittest.mock import patch +from aiohttp import web import pytest from homeassistant import const +from homeassistant.bootstrap import DATA_LOGGING import homeassistant.core as ha from homeassistant.setup import async_setup_component +from tests.common import mock_coro + @pytest.fixture def mock_api_client(hass, aiohttp_client): @@ -398,3 +403,31 @@ def _stream_next_event(stream): def _listen_count(hass): """Return number of event listeners.""" return sum(hass.bus.async_listeners().values()) + + +async def test_api_error_log(hass, aiohttp_client): + """Test if we can fetch the error log.""" + hass.data[DATA_LOGGING] = '/some/path' + await async_setup_component(hass, 'api', { + 'http': { + 'api_password': 'yolo' + } + }) + client = await aiohttp_client(hass.http.app) + + resp = await client.get(const.URL_API_ERROR_LOG) + # Verufy auth required + assert resp.status == 401 + + with patch( + 'homeassistant.components.http.view.HomeAssistantView.file', + return_value=mock_coro(web.Response(status=200, text='Hello')) + ) as mock_file: + resp = await client.get(const.URL_API_ERROR_LOG, headers={ + 'x-ha-access': 'yolo' + }) + + assert len(mock_file.mock_calls) == 1 + assert mock_file.mock_calls[0][1][1] == hass.data[DATA_LOGGING] + assert resp.status == 200 + assert await resp.text() == 'Hello' From c36c2be37218a1e97a6c4c1b9d3c5dd23e873c6b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Apr 2018 16:52:50 -0400 Subject: [PATCH 136/136] Version bump to 0.67.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 53c72a46c3f..5364fe6951e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 67 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)