From 54dfe045b211b6c65bac67be01217c8dcb323fdd Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 May 2018 10:04:00 +0200 Subject: [PATCH 001/137] Upgrade aiohttp to 3.2.1 (#14517) * Upgrade aiohttp to 3.2.1 * Upgrade async_timeout to 3.0.0 * Update the order of the requirements --- homeassistant/package_constraints.txt | 20 ++++++++++---------- requirements_all.txt | 20 ++++++++++---------- setup.py | 20 ++++++++++---------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4a7df44ee5e..e76dc24d9dd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,15 +1,15 @@ -requests==2.18.4 -pyyaml>=3.11,<4 -pytz>=2018.04 -pip>=8.0.3 -jinja2>=2.10 -voluptuous==0.11.1 -typing>=3,<4 -aiohttp==3.1.3 -async_timeout==2.0.1 +aiohttp==3.2.1 astral==1.6.1 -certifi>=2018.04.16 +async_timeout==3.0.0 attrs==18.1.0 +certifi>=2018.04.16 +jinja2>=2.10 +pip>=8.0.3 +pytz>=2018.04 +pyyaml>=3.11,<4 +requests==2.18.4 +typing>=3,<4 +voluptuous==0.11.1 # Breaks Python 3.6 and is not needed for our supported Python versions enum34==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5d71464c08a..9bcf496df09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,16 +1,16 @@ # Home Assistant core -requests==2.18.4 -pyyaml>=3.11,<4 -pytz>=2018.04 -pip>=8.0.3 -jinja2>=2.10 -voluptuous==0.11.1 -typing>=3,<4 -aiohttp==3.1.3 -async_timeout==2.0.1 +aiohttp==3.2.1 astral==1.6.1 -certifi>=2018.04.16 +async_timeout==3.0.0 attrs==18.1.0 +certifi>=2018.04.16 +jinja2>=2.10 +pip>=8.0.3 +pytz>=2018.04 +pyyaml>=3.11,<4 +requests==2.18.4 +typing>=3,<4 +voluptuous==0.11.1 # homeassistant.components.nuimo_controller --only-binary=all https://github.com/getSenic/nuimo-linux-python/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip#nuimo==1.0.0 diff --git a/setup.py b/setup.py index 2469f32d77e..4390b980f9e 100755 --- a/setup.py +++ b/setup.py @@ -42,18 +42,18 @@ DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ - 'requests==2.18.4', - 'pyyaml>=3.11,<4', - 'pytz>=2018.04', - 'pip>=8.0.3', - 'jinja2>=2.10', - 'voluptuous==0.11.1', - 'typing>=3,<4', - 'aiohttp==3.1.3', - 'async_timeout==2.0.1', + 'aiohttp==3.2.1', 'astral==1.6.1', - 'certifi>=2018.04.16', + 'async_timeout==3.0.0', 'attrs==18.1.0', + 'certifi>=2018.04.16', + 'jinja2>=2.10', + 'pip>=8.0.3', + 'pytz>=2018.04', + 'pyyaml>=3.11,<4', + 'requests==2.18.4', + 'typing>=3,<4', + 'voluptuous==0.11.1', ] MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) From daf8143d01faf20a72d29c4911057ff2505a0da6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 May 2018 10:04:20 +0200 Subject: [PATCH 002/137] Upgrade youtube_dl to 2018.05.18 (#14519) --- 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 89cc296111b..bef02d7113f 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.05.09'] +REQUIREMENTS = ['youtube_dl==2018.05.18'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9bcf496df09..37b51bc7fef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1376,7 +1376,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.05.09 +youtube_dl==2018.05.18 # homeassistant.components.light.zengge zengge==0.2 From 46dc9322a277ad654476f3ce3986f792c6f91944 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 May 2018 10:04:42 +0200 Subject: [PATCH 003/137] Upgrade keyring to 12.2.1 (#14521) --- homeassistant/scripts/keyring.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 11e337a76b5..e02305b5fbb 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==12.2.0', 'keyrings.alt==3.1'] +REQUIREMENTS = ['keyring==12.2.1', 'keyrings.alt==3.1'] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 37b51bc7fef..030ab842164 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -462,7 +462,7 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.6 # homeassistant.scripts.keyring -keyring==12.2.0 +keyring==12.2.1 # homeassistant.scripts.keyring keyrings.alt==3.1 From 8deb4624719c22ec0e6bc6ba3eab305dabb31fe9 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 May 2018 10:05:02 +0200 Subject: [PATCH 004/137] Upgrade restrictedpython to 4.0b4 (#14537) --- homeassistant/components/python_script.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py index 1d33740d4a4..bbc6e07f2b0 100644 --- a/homeassistant/components/python_script.py +++ b/homeassistant/components/python_script.py @@ -18,7 +18,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename import homeassistant.util.dt as dt_util -REQUIREMENTS = ['restrictedpython==4.0b3'] +REQUIREMENTS = ['restrictedpython==4.0b4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 030ab842164..f2f9ceff9ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1132,7 +1132,7 @@ raincloudy==0.0.4 regenmaschine==0.4.1 # homeassistant.components.python_script -restrictedpython==4.0b3 +restrictedpython==4.0b4 # homeassistant.components.rflink rflink==0.0.37 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a0db70f7d7..da12798b8f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -168,7 +168,7 @@ pyupnp-async==0.1.0.2 pywebpush==1.6.0 # homeassistant.components.python_script -restrictedpython==4.0b3 +restrictedpython==4.0b4 # homeassistant.components.rflink rflink==0.0.37 From aa51bb6cb9df15e432581bd251f3f7b5f6a7cb0b Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Sat, 19 May 2018 09:49:52 +0100 Subject: [PATCH 005/137] Bump pyvera version (improve stability of poll loop). (#14540) --- homeassistant/components/vera.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index 5cc4de0d5ca..ebe92a2dcc2 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -19,7 +19,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_LIGHTS, CONF_EXCLUDE) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.42'] +REQUIREMENTS = ['pyvera==0.2.43'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f2f9ceff9ec..193deab8a87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ pyupnp-async==0.1.0.2 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.2.42 +pyvera==0.2.43 # homeassistant.components.switch.vesync pyvesync==0.1.1 From 74f1f08ab51ea96b340ed6592402a9ffcc85e34c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 May 2018 10:44:54 -0400 Subject: [PATCH 006/137] Bump frontend to 20180519.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 8cc3c8ea473..d4700e5edd3 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180518.1'] +REQUIREMENTS = ['home-assistant-frontend==20180519.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 193deab8a87..b22ba13dcb5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180518.1 +home-assistant-frontend==20180519.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da12798b8f7..5d4d91b1037 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180518.1 +home-assistant-frontend==20180519.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From e88fc33eef861490a158a424d3e8d4fae187e964 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 May 2018 17:14:53 +0200 Subject: [PATCH 007/137] Fix sensor name (fixes #14535) (#14541) --- homeassistant/components/sensor/bom.py | 47 ++++++++++++++++---------- tests/components/sensor/test_bom.py | 26 +++++++------- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index d6764e5e994..5cec528d26a 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -28,6 +28,12 @@ import homeassistant.helpers.config_validation as cv _RESOURCE = 'http://www.bom.gov.au/fwo/{}/{}.{}.json' _LOGGER = logging.getLogger(__name__) +ATTR_LAST_UPDATE = 'last_update' +ATTR_SENSOR_ID = 'sensor_id' +ATTR_STATION_ID = 'station_id' +ATTR_STATION_NAME = 'station_name' +ATTR_ZONE_ID = 'zone_id' + CONF_ATTRIBUTION = "Data provided by the Australian Bureau of Meteorology" CONF_STATION = 'station' CONF_ZONE_ID = 'zone_id' @@ -35,7 +41,6 @@ CONF_WMO_ID = 'wmo_id' MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=35) -# Sensor types are defined like: Name, units SENSOR_TYPES = { 'wmo': ['wmo', None], 'name': ['Station Name', None], @@ -70,7 +75,7 @@ SENSOR_TYPES = { 'weather': ['Weather', None], 'wind_dir': ['Wind Direction', None], 'wind_spd_kmh': ['Wind Speed kmh', 'km/h'], - 'wind_spd_kt': ['Wind Direction kt', 'kt'] + 'wind_spd_kt': ['Wind Speed kt', 'kt'] } @@ -98,6 +103,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the BOM sensor.""" station = config.get(CONF_STATION) zone_id, wmo_id = config.get(CONF_ZONE_ID), config.get(CONF_WMO_ID) + if station is not None: if zone_id and wmo_id: _LOGGER.warning( @@ -111,17 +117,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.config.config_dir) if station is None: _LOGGER.error("Could not get BOM weather station from lat/lon") - return False + return bom_data = BOMCurrentData(hass, station) + try: bom_data.update() except ValueError as err: - _LOGGER.error("Received error from BOM_Current: %s", err) - return False + _LOGGER.error("Received error from BOM Current: %s", err) + return + add_devices([BOMCurrentSensor(bom_data, variable, config.get(CONF_NAME)) for variable in config[CONF_MONITORED_CONDITIONS]]) - return True class BOMCurrentSensor(Entity): @@ -150,14 +157,17 @@ class BOMCurrentSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the device.""" - attr = {} - attr['Sensor Id'] = self._condition - attr['Zone Id'] = self.bom_data.latest_data['history_product'] - attr['Station Id'] = self.bom_data.latest_data['wmo'] - attr['Station Name'] = self.bom_data.latest_data['name'] - attr['Last Update'] = datetime.datetime.strptime(str( - self.bom_data.latest_data['local_date_time_full']), '%Y%m%d%H%M%S') - attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attr = { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_LAST_UPDATE: datetime.datetime.strptime( + str(self.bom_data.latest_data['local_date_time_full']), + '%Y%m%d%H%M%S'), + ATTR_SENSOR_ID: self._condition, + ATTR_STATION_ID: self.bom_data.latest_data['wmo'], + ATTR_STATION_NAME: self.bom_data.latest_data['name'], + ATTR_ZONE_ID: self.bom_data.latest_data['history_product'], + } + return attr @property @@ -180,8 +190,9 @@ class BOMCurrentData(object): self._data = None def _build_url(self): + """Build the URL for the requests.""" url = _RESOURCE.format(self._zone_id, self._zone_id, self._wmo_id) - _LOGGER.info("BOM URL %s", url) + _LOGGER.debug("BOM URL: %s", url) return url @property @@ -200,7 +211,7 @@ class BOMCurrentData(object): for the latest value that is not `-`. Iterators are used in this method to avoid iterating needlessly - iterating through the entire BOM provided dataset + iterating through the entire BOM provided dataset. """ condition_readings = (entry[condition] for entry in self._data) return next((x for x in condition_readings if x != '-'), None) @@ -257,7 +268,7 @@ def _get_bom_stations(): def bom_stations(cache_dir): """Return {CONF_STATION: (lat, lon)} for all stations, for auto-config. - Results from internet requests are cached as compressed json, making + Results from internet requests are cached as compressed JSON, making subsequent calls very much faster. """ cache_file = os.path.join(cache_dir, '.bom-stations.json.gz') @@ -277,7 +288,7 @@ def closest_station(lat, lon, cache_dir): stations = bom_stations(cache_dir) def comparable_dist(wmo_id): - """Create a psudeo-distance from lat/lon.""" + """Create a psudeo-distance from latitude/longitude.""" station_lat, station_lon = stations[wmo_id] return (lat - station_lat) ** 2 + (lon - station_lon) ** 2 diff --git a/tests/components/sensor/test_bom.py b/tests/components/sensor/test_bom.py index 06a7089e052..5e5a829662a 100644 --- a/tests/components/sensor/test_bom.py +++ b/tests/components/sensor/test_bom.py @@ -1,16 +1,16 @@ """The tests for the BOM Weather sensor platform.""" +import json import re import unittest -import json -import requests from unittest.mock import patch from urllib.parse import urlparse -from homeassistant.setup import setup_component -from homeassistant.components import sensor - +import requests from tests.common import ( - get_test_home_assistant, assert_setup_component, load_fixture) + assert_setup_component, get_test_home_assistant, load_fixture) + +from homeassistant.components import sensor +from homeassistant.setup import setup_component VALID_CONFIG = { 'platform': 'bom', @@ -89,9 +89,11 @@ class TestBOMWeatherSensor(unittest.TestCase): self.assertTrue(setup_component( self.hass, sensor.DOMAIN, {'sensor': VALID_CONFIG})) - self.assertEqual('Fine', self.hass.states.get( - 'sensor.bom_fake_weather').state) - self.assertEqual('1021.7', self.hass.states.get( - 'sensor.bom_fake_pressure_mb').state) - self.assertEqual('25.0', self.hass.states.get( - 'sensor.bom_fake_feels_like_c').state) + weather = self.hass.states.get('sensor.bom_fake_weather').state + self.assertEqual('Fine', weather) + + pressure = self.hass.states.get('sensor.bom_fake_pressure_mb').state + self.assertEqual('1021.7', pressure) + + feels_like = self.hass.states.get('sensor.bom_fake_feels_like_c').state + self.assertEqual('25.0', feels_like) From c316d5b0b93a51a49750d8df0503df6a34c70189 Mon Sep 17 00:00:00 2001 From: Kerwin Bryant Date: Sun, 20 May 2018 04:36:47 +0800 Subject: [PATCH 008/137] Add support to ignore a xiaomi aqara gateway (#14428) --- homeassistant/components/xiaomi_aqara.py | 4 +++- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 2cbf977443c..27bd496a3f0 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.9.3'] +REQUIREMENTS = ['PyXiaomiGateway==0.9.4'] _LOGGER = logging.getLogger(__name__) @@ -36,6 +36,7 @@ CONF_DISCOVERY_RETRY = 'discovery_retry' CONF_GATEWAYS = 'gateways' CONF_INTERFACE = 'interface' CONF_KEY = 'key' +CONF_DISABLE = 'disable' DOMAIN = 'xiaomi_aqara' @@ -73,6 +74,7 @@ GATEWAY_CONFIG = vol.Schema({ vol.All(cv.string, vol.Length(min=16, max=16)), vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=9898): cv.port, + vol.Optional(CONF_DISABLE, default=False): cv.boolean, }) diff --git a/requirements_all.txt b/requirements_all.txt index b22ba13dcb5..8b5fb8c48be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.9.3 +PyXiaomiGateway==0.9.4 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From c8a53c564a9c29e6958791c570bbee2a1837a1f9 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 20 May 2018 00:33:52 +0200 Subject: [PATCH 009/137] Wait for future mysensors gateway ready (#14398) * Wait for future mysensors gateway ready * Add an asyncio future that is done when the gateway reports the gateway ready message, I_GATEWAY_READY. * This will make sure that the gateway is ready before home assistant fires the home assistant start event. Automations can now send messages to the gateway when home assistant is started. * Use async timeout to wait max 15 seconds for ready gateway. * Address comments --- homeassistant/components/mysensors.py | 46 +++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 6721669a026..1e7e252bd9d 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -12,13 +12,14 @@ import socket import sys from timeit import default_timer as timer +import async_timeout import voluptuous as vol from homeassistant.components.mqtt import ( valid_publish_topic, valid_subscribe_topic) from homeassistant.const import ( - ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, - EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) + ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP, + STATE_OFF, STATE_ON) from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -57,9 +58,11 @@ DEFAULT_TCP_PORT = 5003 DEFAULT_VERSION = '1.4' DOMAIN = 'mysensors' +GATEWAY_READY_TIMEOUT = 15.0 MQTT_COMPONENT = 'mqtt' MYSENSORS_GATEWAYS = 'mysensors_gateways' MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}' +MYSENSORS_GATEWAY_READY = 'mysensors_gateway_ready_{}' PLATFORM = 'platform' SCHEMA = 'schema' SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}' @@ -353,12 +356,12 @@ async def async_setup(hass, config): tcp_port = gway.get(CONF_TCP_PORT) in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '') out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '') - ready_gateway = await setup_gateway( + gateway = await setup_gateway( device, persistence_file, baud_rate, tcp_port, in_prefix, out_prefix) - if ready_gateway is not None: - ready_gateway.nodes_config = gway.get(CONF_NODES) - gateways[id(ready_gateway)] = ready_gateway + if gateway is not None: + gateway.nodes_config = gway.get(CONF_NODES) + gateways[id(gateway)] = gateway if not gateways: _LOGGER.error( @@ -395,6 +398,35 @@ async def gw_start(hass, gateway): await gateway.start() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop) + if gateway.device == 'mqtt': + # Gatways connected via mqtt doesn't send gateway ready message. + return + gateway_ready = asyncio.Future() + gateway_ready_key = MYSENSORS_GATEWAY_READY.format(id(gateway)) + hass.data[gateway_ready_key] = gateway_ready + + try: + with async_timeout.timeout(GATEWAY_READY_TIMEOUT, loop=hass.loop): + await gateway_ready + except asyncio.TimeoutError: + _LOGGER.warning( + "Gateway %s not ready after %s secs so continuing with setup", + gateway.device, GATEWAY_READY_TIMEOUT) + finally: + hass.data.pop(gateway_ready_key, None) + + +@callback +def set_gateway_ready(hass, msg): + """Set asyncio future result if gateway is ready.""" + if (msg.type != msg.gateway.const.MessageType.internal or + msg.sub_type != msg.gateway.const.Internal.I_GATEWAY_READY): + return + gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format( + id(msg.gateway))) + if gateway_ready is None or gateway_ready.cancelled(): + return + gateway_ready.set_result(True) def validate_child(gateway, node_id, child): @@ -495,6 +527,8 @@ def gw_callback_factory(hass): _LOGGER.debug( "Node update: node %s child %s", msg.node_id, msg.child_id) + set_gateway_ready(hass, msg) + try: child = msg.gateway.sensors[msg.node_id].children[msg.child_id] except KeyError: From c050eb4100368b272ecbeab06c4ffaf1982f4bcd Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 20 May 2018 09:50:12 +0200 Subject: [PATCH 010/137] Pushed to version 0.7.2 of denonavr (#14551) --- homeassistant/components/media_player/denonavr.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index fe8fc46c24b..74d3c5a0785 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['denonavr==0.6.1'] +REQUIREMENTS = ['denonavr==0.7.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8b5fb8c48be..7d4b4bb3f41 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -246,7 +246,7 @@ defusedxml==0.5.0 deluge-client==1.4.0 # homeassistant.components.media_player.denonavr -denonavr==0.6.1 +denonavr==0.7.2 # homeassistant.components.media_player.directv directpy==0.2 From c8ad9c4daa7cbc2f596a2a3c00278d6539cd79b3 Mon Sep 17 00:00:00 2001 From: Marco Orovecchia Date: Sun, 20 May 2018 20:53:57 +0200 Subject: [PATCH 011/137] Add auto discovery for nanoleaf aurora lights (#14301) * auto discovery added for nanoleaf aurora lights * changes requested by review * visual indentation * line too long * hide autocreated config --- homeassistant/components/discovery.py | 1 + .../components/light/nanoleaf_aurora.py | 47 ++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index a24e82da106..69447b81cd4 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -83,6 +83,7 @@ SERVICE_HANDLERS = { 'songpal': ('media_player', 'songpal'), 'kodi': ('media_player', 'kodi'), 'volumio': ('media_player', 'volumio'), + 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), } OPTIONAL_SERVICE_HANDLERS = { diff --git a/homeassistant/components/light/nanoleaf_aurora.py b/homeassistant/components/light/nanoleaf_aurora.py index 99c07166037..8b0b7c053c8 100644 --- a/homeassistant/components/light/nanoleaf_aurora.py +++ b/homeassistant/components/light/nanoleaf_aurora.py @@ -17,6 +17,7 @@ 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 +from homeassistant.util.json import load_json, save_json REQUIREMENTS = ['nanoleaf==0.4.1'] @@ -24,6 +25,10 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Aurora' +DATA_NANOLEAF_AURORA = 'nanoleaf_aurora' + +CONFIG_FILE = '.nanoleaf_aurora.conf' + ICON = 'mdi:triangle-outline' SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | @@ -39,31 +44,59 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Nanoleaf Aurora device.""" import nanoleaf - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + import nanoleaf.setup + if DATA_NANOLEAF_AURORA not in hass.data: + hass.data[DATA_NANOLEAF_AURORA] = dict() + + token = '' + if discovery_info is not None: + host = discovery_info['host'] + name = discovery_info['hostname'] + # if device already exists via config, skip discovery setup + if host in hass.data[DATA_NANOLEAF_AURORA]: + return + _LOGGER.info("Discovered a new Aurora: %s", discovery_info) + conf = load_json(hass.config.path(CONFIG_FILE)) + if conf.get(host, {}).get('token'): + token = conf[host]['token'] + else: + host = config[CONF_HOST] + name = config[CONF_NAME] + token = config[CONF_TOKEN] + + if not token: + token = nanoleaf.setup.generate_auth_token(host) + if not token: + _LOGGER.error("Could not generate the auth token, did you press " + "and hold the power button on %s" + "for 5-7 seconds?", name) + return + conf = load_json(hass.config.path(CONFIG_FILE)) + conf[host] = {'token': token} + save_json(hass.config.path(CONFIG_FILE), conf) + 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) return - add_devices([AuroraLight(aurora_light)], True) + hass.data[DATA_NANOLEAF_AURORA][host] = aurora_light + add_devices([AuroraLight(aurora_light, name)], True) class AuroraLight(Light): """Representation of a Nanoleaf Aurora.""" - def __init__(self, light): + def __init__(self, light, name): """Initialize an Aurora light.""" self._brightness = None self._color_temp = None self._effect = None self._effects_list = None self._light = light - self._name = light.hass_name + self._name = name self._hs_color = None self._state = None From 4395217031f41b86ed54e1083998b03448ab9222 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 20 May 2018 19:00:51 -0400 Subject: [PATCH 012/137] zha: Don't poll switch devices (#14560) --- homeassistant/components/binary_sensor/zha.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index d3b31188760..0fd9db19d1a 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -211,6 +211,11 @@ class Switch(zha.Entity, BinarySensorDevice): general.LevelControl.cluster_id: self.LevelListener(self), } + @property + def should_poll(self) -> bool: + """Let zha handle polling.""" + return False + @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" From b489519930750bbee1acb1f2d1e98c1ff9241be0 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 20 May 2018 19:01:56 -0400 Subject: [PATCH 013/137] zha: Add metering sensor (#14562) --- homeassistant/components/sensor/zha.py | 22 ++++++++++++++++++++++ homeassistant/components/zha/const.py | 1 + 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 3ca908a679d..abb4c651e78 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -34,6 +34,7 @@ def make_sensor(discovery_info): from zigpy.zcl.clusters.measurement import ( RelativeHumidity, TemperatureMeasurement, PressureMeasurement ) + from zigpy.zcl.clusters.smartenergy import Metering in_clusters = discovery_info['in_clusters'] if RelativeHumidity.cluster_id in in_clusters: sensor = RelativeHumiditySensor(**discovery_info) @@ -41,6 +42,8 @@ def make_sensor(discovery_info): sensor = TemperatureSensor(**discovery_info) elif PressureMeasurement.cluster_id in in_clusters: sensor = PressureSensor(**discovery_info) + elif Metering.cluster_id in in_clusters: + sensor = MeteringSensor(**discovery_info) else: sensor = Sensor(**discovery_info) @@ -143,3 +146,22 @@ class PressureSensor(Sensor): return None return round(float(self._state)) + + +class MeteringSensor(Sensor): + """ZHA Metering sensor.""" + + value_attribute = 1024 + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return 'W' + + @property + def state(self): + """Return the state of the entity.""" + if self._state is None: + return None + + return round(float(self._state)) diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 1c083c3ca93..71f0ea17490 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -48,6 +48,7 @@ def populate_data(): zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', zcl.clusters.measurement.PressureMeasurement: 'sensor', + zcl.clusters.smartenergy.Metering: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', }) From ee7e59fe687cf73163fa18ea02c74626eb6062e2 Mon Sep 17 00:00:00 2001 From: damarco Date: Mon, 21 May 2018 01:14:18 +0200 Subject: [PATCH 014/137] zha: Set default binary_sensor state to false (#14553) --- homeassistant/components/binary_sensor/zha.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index 0fd9db19d1a..6931355ca0e 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -203,8 +203,8 @@ class Switch(zha.Entity, BinarySensorDevice): def __init__(self, **kwargs): """Initialize Switch.""" super().__init__(**kwargs) - self._state = True - self._level = 255 + self._state = False + self._level = 0 from zigpy.zcl.clusters import general self._out_listeners = { general.OnOff.cluster_id: self.OnOffListener(self), From 0589379de5462567ba3f8b7edd31d22a5f08f41b Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 May 2018 04:25:53 +0200 Subject: [PATCH 015/137] Homekit style cleanup (#14556) * Style cleanup * Sorted imports * Harmonized service calls * Test improvements * Small update --- homeassistant/components/homekit/__init__.py | 11 ++-- .../components/homekit/accessories.py | 5 +- homeassistant/components/homekit/const.py | 35 +++++------ .../components/homekit/type_covers.py | 25 ++++---- homeassistant/components/homekit/type_fans.py | 13 ++-- .../components/homekit/type_lights.py | 42 ++++++------- .../components/homekit/type_locks.py | 7 +-- .../homekit/type_security_systems.py | 12 ++-- .../components/homekit/type_sensors.py | 38 ++++++------ .../components/homekit/type_switches.py | 4 +- .../components/homekit/type_thermostats.py | 60 ++++++++++--------- tests/components/homekit/common.py | 8 +++ tests/components/homekit/test_accessories.py | 39 ++++++------ .../homekit/test_get_accessories.py | 15 ++--- tests/components/homekit/test_homekit.py | 49 +++++++-------- tests/components/homekit/test_type_covers.py | 4 +- tests/components/homekit/test_type_fans.py | 12 ++-- tests/components/homekit/test_type_lights.py | 6 +- tests/components/homekit/test_type_locks.py | 2 +- .../homekit/test_type_security_systems.py | 10 ++-- tests/components/homekit/test_type_sensors.py | 8 +-- .../components/homekit/test_type_switches.py | 2 +- .../homekit/test_type_thermostats.py | 8 +-- tests/components/homekit/test_util.py | 8 +-- 24 files changed, 209 insertions(+), 214 deletions(-) create mode 100644 tests/components/homekit/common.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ce5f30d7bf2..202f9694689 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -13,17 +13,18 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, + CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) + TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry from .const import ( - CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_PORT, - DEFAULT_AUTO_START, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, - DEVICE_CLASS_CO2, DEVICE_CLASS_PM25) + CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START, + DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, + SERVICE_HOMEKIT_START) from .util import show_setup_message, validate_entity_config TYPES = Registry() diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ff835659221..ded4526b008 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -84,20 +84,21 @@ class HomeAccessory(Accessory): async_track_state_change( self.hass, self.entity_id, self.update_state_callback) + @ha_callback def update_state_callback(self, entity_id=None, old_state=None, new_state=None): """Callback from state change listener.""" _LOGGER.debug('New_state: %s', new_state) if new_state is None: return - self.update_state(new_state) + self.hass.async_add_job(self.update_state, new_state) def update_state(self, new_state): """Method called on state change to update HomeKit value. Overridden by accessory types. """ - pass + raise NotImplementedError() class HomeBridge(Bridge): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index adde13cc030..21cad2d9cf7 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,23 +1,23 @@ """Constants used be the HomeKit component.""" -# #### MISC #### +# #### Misc #### DEBOUNCE_TIMEOUT = 0.5 DOMAIN = 'homekit' HOMEKIT_FILE = '.homekit.state' HOMEKIT_NOTIFY_ID = 4663548 -# #### CONFIG #### +# #### Config #### CONF_AUTO_START = 'auto_start' CONF_ENTITY_CONFIG = 'entity_config' CONF_FILTER = 'filter' -# #### CONFIG DEFAULTS #### +# #### Config Defaults #### DEFAULT_AUTO_START = True DEFAULT_PORT = 51827 -# #### HOMEKIT COMPONENT SERVICES #### +# #### HomeKit Component Services #### SERVICE_HOMEKIT_START = 'start' -# #### STRING CONSTANTS #### +# #### String Constants #### BRIDGE_MODEL = 'Bridge' BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' @@ -31,10 +31,10 @@ SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' SERV_CONTACT_SENSOR = 'ContactSensor' SERV_FANV2 = 'Fanv2' SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener' -SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity +SERV_HUMIDITY_SENSOR = 'HumiditySensor' SERV_LEAK_SENSOR = 'LeakSensor' SERV_LIGHT_SENSOR = 'LightSensor' -SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name +SERV_LIGHTBULB = 'Lightbulb' SERV_LOCK = 'LockMechanism' SERV_MOTION_SENSOR = 'MotionSensor' SERV_OCCUPANCY_SENSOR = 'OccupancySensor' @@ -44,13 +44,12 @@ SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' SERV_WINDOW_COVERING = 'WindowCovering' -# CurrentPosition, TargetPosition, PositionState # #### Characteristics #### CHAR_ACTIVE = 'Active' CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_QUALITY = 'AirQuality' -CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] +CHAR_BRIGHTNESS = 'Brightness' CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel' CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel' @@ -61,13 +60,13 @@ CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel' CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' -CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100] -CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent +CHAR_CURRENT_POSITION = 'CurrentPosition' +CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_FIRMWARE_REVISION = 'FirmwareRevision' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' -CHAR_HUE = 'Hue' # arcdegress | [0, 360] +CHAR_HUE = 'Hue' CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_LOCK_CURRENT_STATE = 'LockCurrentState' CHAR_LOCK_TARGET_STATE = 'LockTargetState' @@ -77,16 +76,16 @@ CHAR_MODEL = 'Model' CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_NAME = 'Name' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' -CHAR_ON = 'On' # boolean +CHAR_ON = 'On' CHAR_POSITION_STATE = 'PositionState' CHAR_ROTATION_DIRECTION = 'RotationDirection' -CHAR_SATURATION = 'Saturation' # percent +CHAR_SATURATION = 'Saturation' CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SMOKE_DETECTED = 'SmokeDetected' CHAR_SWING_MODE = 'SwingMode' CHAR_TARGET_DOOR_STATE = 'TargetDoorState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' -CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100] +CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' @@ -94,21 +93,17 @@ CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' # #### Properties #### PROP_MAX_VALUE = 'maxValue' PROP_MIN_VALUE = 'minValue' - PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} -# #### Device Class #### +# #### Device Classes #### DEVICE_CLASS_CO2 = 'co2' DEVICE_CLASS_DOOR = 'door' DEVICE_CLASS_GARAGE_DOOR = 'garage_door' DEVICE_CLASS_GAS = 'gas' -DEVICE_CLASS_HUMIDITY = 'humidity' -DEVICE_CLASS_LIGHT = 'light' DEVICE_CLASS_MOISTURE = 'moisture' DEVICE_CLASS_MOTION = 'motion' DEVICE_CLASS_OCCUPANCY = 'occupancy' DEVICE_CLASS_OPENING = 'opening' DEVICE_CLASS_PM25 = 'pm25' DEVICE_CLASS_SMOKE = 'smoke' -DEVICE_CLASS_TEMPERATURE = 'temperature' DEVICE_CLASS_WINDOW = 'window' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index a32ba0370ec..cf0620a4e30 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -1,21 +1,21 @@ """Class to hold all cover accessories.""" import logging -from pyhap.const import CATEGORY_WINDOW_COVERING, CATEGORY_GARAGE_DOOR_OPENER +from pyhap.const import CATEGORY_GARAGE_DOOR_OPENER, CATEGORY_WINDOW_COVERING from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED, - SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER, - ATTR_SUPPORTED_FEATURES) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, + STATE_CLOSED, STATE_OPEN) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, - CHAR_TARGET_POSITION, CHAR_POSITION_STATE, - SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) + CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, CHAR_POSITION_STATE, + CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION, + SERV_GARAGE_DOOR_OPENER, SERV_WINDOW_COVERING) _LOGGER = logging.getLogger(__name__) @@ -44,12 +44,13 @@ class GarageDoorOpener(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self.flag_target_state = True + params = {ATTR_ENTITY_ID: self.entity_id} if value == 0: self.char_current_state.set_value(3) - self.hass.components.cover.open_cover(self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_OPEN_COVER, params) elif value == 1: self.char_current_state.set_value(2) - self.hass.components.cover.close_cover(self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, params) def update_state(self, new_state): """Update cover state after state changed.""" @@ -141,8 +142,8 @@ class WindowCoveringBasic(HomeAccessory): else: service, position = (SERVICE_CLOSE_COVER, 0) - self.hass.services.call(DOMAIN, service, - {ATTR_ENTITY_ID: self.entity_id}) + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) # Snap the current/target position to the expected final position. self.char_current_position.set_value(position) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index a3ea027c07e..bf0d4da6a59 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -4,12 +4,12 @@ import logging from pyhap.const import CATEGORY_FAN from homeassistant.components.fan import ( - ATTR_DIRECTION, ATTR_OSCILLATING, - DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, - SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) + ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE, + DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, + SUPPORT_OSCILLATE) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, - SERVICE_TURN_OFF, SERVICE_TURN_ON) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_OFF, STATE_ON) from . import TYPES from .accessories import HomeAccessory @@ -71,8 +71,7 @@ class Fan(HomeAccessory): _LOGGER.debug('%s: Set direction to %d', self.entity_id, value) self._flag[CHAR_ROTATION_DIRECTION] = True direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD - params = {ATTR_ENTITY_ID: self.entity_id, - ATTR_DIRECTION: direction} + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_DIRECTION: direction} self.hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, params) def set_oscillating(self, value): diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index dae3579a97a..da012799602 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -4,16 +4,18 @@ import logging from pyhap.const import CATEGORY_LIGHTBULB from homeassistant.components.light import ( - 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 + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, DOMAIN, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON, + SERVICE_TURN_OFF, STATE_OFF, STATE_ON) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, - CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION, - PROP_MAX_VALUE, PROP_MIN_VALUE) + CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, CHAR_ON, + CHAR_SATURATION, SERV_LIGHTBULB, PROP_MAX_VALUE, PROP_MIN_VALUE) _LOGGER = logging.getLogger(__name__) @@ -79,28 +81,27 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self._flag[CHAR_ON] = True - - if value == 1: - self.hass.components.light.turn_on(self.entity_id) - elif value == 0: - self.hass.components.light.turn_off(self.entity_id) + params = {ATTR_ENTITY_ID: self.entity_id} + service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF + self.hass.services.call(DOMAIN, service, params) @debounce def set_brightness(self, value): """Set brightness if call came from HomeKit.""" _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value) self._flag[CHAR_BRIGHTNESS] = True - 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) + if value == 0: + self.set_state(0) # Turn off light + return + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) 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.hass.components.light.turn_on(self.entity_id, color_temp=value) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" @@ -118,15 +119,14 @@ class Light(HomeAccessory): def set_color(self): """Set color if call came from HomeKit.""" - # Handle Color 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) self._flag.update({ CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) - self.hass.components.light.turn_on( - self.entity_id, hs_color=color) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def update_state(self, new_state): """Update light after state change.""" diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 309f3072768..05ab6c6f822 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -4,13 +4,12 @@ import logging from pyhap.const import CATEGORY_DOOR_LOCK from homeassistant.components.lock import ( - ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) + ATTR_ENTITY_ID, DOMAIN, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) from homeassistant.const import ATTR_CODE from . import TYPES from .accessories import HomeAccessory -from .const import ( - SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) +from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK _LOGGER = logging.getLogger(__name__) @@ -55,7 +54,7 @@ class Lock(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} if self._code: params[ATTR_CODE] = self._code - self.hass.services.call('lock', service, params) + self.hass.services.call(DOMAIN, service, params) def update_state(self, new_state): """Update lock after state changed.""" diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index bd29453e10a..bbf8b3f17cb 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -3,16 +3,16 @@ import logging from pyhap.const import CATEGORY_ALARM_SYSTEM +from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, ATTR_ENTITY_ID, ATTR_CODE) + ATTR_ENTITY_ID, ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, STATE_ALARM_DISARMED) from . import TYPES from .accessories import HomeAccessory from .const import ( - SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE, - CHAR_TARGET_SECURITY_STATE) + CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE, + SERV_SECURITY_SYSTEM) _LOGGER = logging.getLogger(__name__) @@ -56,7 +56,7 @@ class SecuritySystem(HomeAccessory): 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(DOMAIN, service, params) def update_state(self, new_state): """Update security state after state changed.""" diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 0005c6184ee..373c1188f2d 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -4,26 +4,26 @@ import logging from pyhap.const import CATEGORY_SENSOR from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, - ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_HOME, + TEMP_CELSIUS) from . import TYPES from .accessories import HomeAccessory from .const import ( - SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, - CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, - SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY, - CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL, - SERV_LIGHT_SENSOR, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, - DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, - DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR, - CHAR_CARBON_MONOXIDE_DETECTED, - DEVICE_CLASS_MOISTURE, SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, - DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, - DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, - DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, - DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_WINDOW, - DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) + CHAR_AIR_PARTICULATE_DENSITY, CHAR_AIR_QUALITY, + CHAR_CARBON_DIOXIDE_DETECTED, CHAR_CARBON_DIOXIDE_LEVEL, + CHAR_CARBON_DIOXIDE_PEAK_LEVEL, CHAR_CARBON_MONOXIDE_DETECTED, + CHAR_CONTACT_SENSOR_STATE, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, + CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, CHAR_LEAK_DETECTED, + CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, CHAR_SMOKE_DETECTED, + DEVICE_CLASS_CO2, DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, + DEVICE_CLASS_WINDOW, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, + SERV_CARBON_DIOXIDE_SENSOR, SERV_CARBON_MONOXIDE_SENSOR, + SERV_CONTACT_SENSOR, SERV_HUMIDITY_SENSOR, SERV_LEAK_SENSOR, + SERV_LIGHT_SENSOR, SERV_MOTION_SENSOR, SERV_OCCUPANCY_SENSOR, + SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR) from .util import ( convert_to_float, temperature_to_homekit, density_to_air_quality) @@ -108,7 +108,7 @@ class AirQualitySensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" density = convert_to_float(new_state.state) - if density is not None: + if density: self.char_density.set_value(density) self.char_quality.set_value(density_to_air_quality(density)) _LOGGER.debug('%s: Set to %d', self.entity_id, density) @@ -134,7 +134,7 @@ class CarbonDioxideSensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" co2 = convert_to_float(new_state.state) - if co2 is not None: + if co2: self.char_co2.set_value(co2) if co2 > self.char_peak.value: self.char_peak.set_value(co2) @@ -157,7 +157,7 @@ class LightSensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" luminance = convert_to_float(new_state.state) - if luminance is not None: + if luminance: self.char_light.set_value(luminance) _LOGGER.debug('%s: Set to %d', self.entity_id, luminance) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index ff4bf1611b8..5754266587c 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -33,9 +33,9 @@ class Switch(HomeAccessory): _LOGGER.debug('%s: Set switch state to %s', self.entity_id, value) self.flag_target_state = True + params = {ATTR_ENTITY_ID: self.entity_id} 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, params) def update_state(self, new_state): """Update switch state after state changed.""" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index ab4d7faf875..d6555d5056d 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -4,22 +4,23 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, - STATE_HEAT, STATE_COOL, STATE_AUTO, SUPPORT_ON_OFF, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) + ATTR_CURRENT_TEMPERATURE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, + ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO, + STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, + TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - 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) + CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, + CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, + CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE, + CHAR_TEMP_DISPLAY_UNITS, SERV_THERMOSTAT) from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -99,12 +100,13 @@ class Thermostat(HomeAccessory): if self.support_power_state is True: params = {ATTR_ENTITY_ID: self.entity_id} if hass_value == STATE_OFF: - self.hass.services.call('climate', 'turn_off', params) + self.hass.services.call(DOMAIN, SERVICE_TURN_OFF, params) return else: - self.hass.services.call('climate', 'turn_on', params) - self.hass.components.climate.set_operation_mode( - operation_mode=hass_value, entity_id=self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_OPERATION_MODE: hass_value} + self.hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, params) @debounce def set_cooling_threshold(self, value): @@ -113,11 +115,11 @@ class Thermostat(HomeAccessory): self.entity_id, value) self.coolingthresh_flag_target_state = True 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) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TARGET_TEMP_HIGH: temperature_to_states(value, self._unit), + ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) @debounce def set_heating_threshold(self, value): @@ -125,13 +127,12 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', self.entity_id, value) self.heatingthresh_flag_target_state = True - # 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) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TARGET_TEMP_HIGH: temperature_to_states(high, self._unit), + ATTR_TARGET_TEMP_LOW: temperature_to_states(value, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) @debounce def set_target_temperature(self, value): @@ -139,9 +140,10 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set target temperature to %.2f°C', self.entity_id, value) self.temperature_flag_target_state = True - value = temperature_to_states(value, self._unit) - self.hass.components.climate.set_temperature( - temperature=value, entity_id=self.entity_id) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TEMPERATURE: temperature_to_states(value, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) def update_state(self, new_state): """Update security state after state changed.""" diff --git a/tests/components/homekit/common.py b/tests/components/homekit/common.py new file mode 100644 index 00000000000..915759f22d6 --- /dev/null +++ b/tests/components/homekit/common.py @@ -0,0 +1,8 @@ +"""Collection of fixtures and functions for the HomeKit tests.""" +from unittest.mock import patch + + +def patch_debounce(): + """Return patch for debounce method.""" + return patch('homeassistant.components.homekit.accessories.debounce', + lambda f: lambda *args, **kwargs: f(*args, **kwargs)) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 1b06e245734..3d1c335f8ae 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -5,22 +5,18 @@ This includes tests for all mock object types. from datetime import datetime, timedelta from unittest.mock import patch, Mock +import pytest + from homeassistant.components.homekit.accessories import ( debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( - BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, SERV_ACCESSORY_INFO, - CHAR_FIRMWARE_REVISION, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, - CHAR_SERIAL_NUMBER, MANUFACTURER) + BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_FIRMWARE_REVISION, + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER, + MANUFACTURER, SERV_ACCESSORY_INFO) from homeassistant.const import __version__, ATTR_NOW, EVENT_TIME_CHANGED import homeassistant.util.dt as dt_util -def patch_debounce(): - """Return patch for debounce method.""" - return patch('homeassistant.components.homekit.accessories.debounce', - lambda f: lambda *args, **kwargs: f(*args, **kwargs)) - - async def test_debounce(hass): """Test add_timeout decorator function.""" def demo_func(*args): @@ -74,20 +70,23 @@ async def test_home_accessory(hass): assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == \ 'homekit.accessory' - hass.states.async_set('homekit.accessory', 'on') - await hass.async_block_till_done() - await hass.async_add_job(acc.run) - hass.states.async_set('homekit.accessory', 'off') + hass.states.async_set(entity_id, 'on') await hass.async_block_till_done() + with patch('homeassistant.components.homekit.accessories.' + 'HomeAccessory.update_state') as mock_update_state: + await hass.async_add_job(acc.run) + state = hass.states.get(entity_id) + mock_update_state.assert_called_with(state) - entity_id = 'test_model.demo' - hass.states.async_set(entity_id, None) - await hass.async_block_till_done() + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert mock_update_state.call_count == 1 - acc = HomeAccessory('hass', 'test_name', entity_id, 2, None) - assert acc.display_name == 'test_name' - assert acc.aid == 2 - assert len(acc.services) == 1 + with pytest.raises(NotImplementedError): + acc.update_state('new_state') + + # Test model name from domain + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, None) serv = acc.services[0] # SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_MODEL).value == 'Test Model' diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 0ffc1ae4767..25a0dd3f1cb 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -4,13 +4,13 @@ from unittest.mock import patch, Mock import pytest from homeassistant.core import State -from homeassistant.components.cover import SUPPORT_OPEN, SUPPORT_CLOSE +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) + ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) def test_not_supported(caplog): @@ -40,14 +40,12 @@ def test_customize_options(config, name): ('Fan', 'fan.test', 'on', {}, {}), ('Light', 'light.test', 'on', {}, {}), ('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}), - + ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, + {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), ('Thermostat', 'climate.test', 'auto', {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), - - ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, - {ATTR_CODE: '1234'}), ]) def test_types(type_name, entity_id, state, attrs, config): """Test if types are associated correctly.""" @@ -83,22 +81,17 @@ def test_type_covers(type_name, entity_id, state, attrs): ('BinarySensor', 'binary_sensor.opening', 'on', {ATTR_DEVICE_CLASS: 'opening'}), ('BinarySensor', 'device_tracker.someone', 'not_home', {}), - ('AirQualitySensor', 'sensor.air_quality_pm25', '40', {}), ('AirQualitySensor', 'sensor.air_quality', '40', {ATTR_DEVICE_CLASS: 'pm25'}), - ('CarbonDioxideSensor', 'sensor.airmeter_co2', '500', {}), ('CarbonDioxideSensor', 'sensor.airmeter', '500', {ATTR_DEVICE_CLASS: 'co2'}), - ('HumiditySensor', 'sensor.humidity', '20', {ATTR_DEVICE_CLASS: 'humidity', ATTR_UNIT_OF_MEASUREMENT: '%'}), - ('LightSensor', 'sensor.light', '900', {ATTR_DEVICE_CLASS: 'illuminance'}), ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lm'}), ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lx'}), - ('TemperatureSensor', 'sensor.temperature', '23', {ATTR_DEVICE_CLASS: 'temperature'}), ('TemperatureSensor', 'sensor.temperature', '23', diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index b22a7f63cda..31337088b33 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -6,29 +6,28 @@ import pytest from homeassistant import setup from homeassistant.core import State from homeassistant.components.homekit import ( - HomeKit, generate_aid, - STATUS_READY, STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT) + generate_aid, HomeKit, STATUS_READY, STATUS_RUNNING, + STATUS_STOPPED, STATUS_WAIT) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( - DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, - DEFAULT_PORT, SERVICE_HOMEKIT_START) + CONF_AUTO_START, DEFAULT_PORT, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START) from homeassistant.helpers.entityfilter import generate_filter from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce IP_ADDRESS = '127.0.0.1' PATH_HOMEKIT = 'homeassistant.components.homekit' -@pytest.fixture('module') -def debounce_patcher(request): +@pytest.fixture(scope='module') +def debounce_patcher(): """Patch debounce method.""" patcher = patch_debounce() - patcher.start() - request.addfinalizer(patcher.stop) + yield patcher.start() + patcher.stop() def test_generate_aid(): @@ -124,27 +123,25 @@ async def test_homekit_setup_ip_address(hass): hass, ANY, port=DEFAULT_PORT, address='172.0.0.0', persist_file=ANY) -async def test_homekit_add_accessory(hass): +async def test_homekit_add_accessory(): """Add accessory if config exists and get_acc returns an accessory.""" - homekit = HomeKit(hass, None, None, lambda entity_id: True, {}) - homekit.bridge = HomeBridge(hass) + homekit = HomeKit('hass', None, None, lambda entity_id: True, {}) + homekit.bridge = mock_bridge = Mock() - with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ - as mock_add_acc, \ - patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: + with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: mock_get_acc.side_effect = [None, 'acc', None] homekit.add_bridge_accessory(State('light.demo', 'on')) - mock_get_acc.assert_called_with(hass, ANY, 363398124, {}) - assert mock_add_acc.called is False + mock_get_acc.assert_called_with('hass', ANY, 363398124, {}) + assert not mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test', 'on')) - mock_get_acc.assert_called_with(hass, ANY, 294192020, {}) - assert mock_add_acc.called is True + mock_get_acc.assert_called_with('hass', ANY, 294192020, {}) + assert mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test_2', 'on')) - mock_get_acc.assert_called_with(hass, ANY, 429982757, {}) - mock_add_acc.assert_called_with('acc') + mock_get_acc.assert_called_with('hass', ANY, 429982757, {}) + mock_bridge.add_accessory.assert_called_with('acc') async def test_homekit_entity_filter(hass): @@ -171,8 +168,8 @@ async def test_homekit_start(hass, debounce_patcher): """Test HomeKit start method.""" pin = b'123-45-678' homekit = HomeKit(hass, None, None, {}, {'cover.demo': {}}) - homekit.bridge = HomeBridge(hass) - homekit.driver = Mock(state=Mock(paired=False, pincode=pin)) + homekit.bridge = Mock() + homekit.driver = mock_driver = Mock(state=Mock(paired=False, pincode=pin)) hass.states.async_set('light.demo', 'on') state = hass.states.async_all()[0] @@ -184,13 +181,13 @@ async def test_homekit_start(hass, debounce_patcher): mock_add_acc.assert_called_with(state) mock_setup_msg.assert_called_with(hass, pin) - assert homekit.driver.start.called is True + assert mock_driver.start.called is True assert homekit.status == STATUS_RUNNING # Test start() if already started - homekit.driver.reset_mock() + mock_driver.reset_mock() await hass.async_add_job(homekit.start) - assert homekit.driver.start.called is False + assert mock_driver.start.called is False async def test_homekit_stop(hass): diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 7260ae40c1a..8138d1c506b 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -4,13 +4,13 @@ from collections import namedtuple import pytest from homeassistant.components.cover import ( - DOMAIN, ATTR_CURRENT_POSITION, ATTR_POSITION, SUPPORT_STOP) + ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index fc504cc6cbd..f96fe19d603 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -4,15 +4,15 @@ from collections import namedtuple import pytest from homeassistant.components.fan import ( - ATTR_DIRECTION, ATTR_OSCILLATING, - DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, - SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) + ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE, + DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, + SUPPORT_DIRECTION, SUPPORT_OSCILLATE) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - STATE_ON, STATE_OFF, STATE_UNKNOWN, SERVICE_TURN_ON, SERVICE_TURN_OFF) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF, + STATE_UNKNOWN, SERVICE_TURN_ON, SERVICE_TURN_OFF) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 65a526edcc3..7a1db7b3f71 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -4,14 +4,14 @@ from collections import namedtuple import pytest from homeassistant.components.light import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, - ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF, STATE_UNKNOWN) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 3b8cde47fcb..f4698b1380b 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -4,7 +4,7 @@ import pytest from homeassistant.components.homekit.type_locks import Lock from homeassistant.components.lock import DOMAIN from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED) + ATTR_CODE, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED) from tests.common import async_mock_service diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 577d2f2175d..7b72404cdaa 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -2,12 +2,12 @@ import pytest from homeassistant.components.alarm_control_panel import DOMAIN -from homeassistant.components.homekit.type_security_systems import ( - SecuritySystem) +from homeassistant.components.homekit.type_security_systems import \ + SecuritySystem from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED) + ATTR_CODE, ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, + STATE_UNKNOWN) from tests.common import async_mock_service diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 56742bada92..e36ae67da12 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,11 +1,11 @@ """Test different accessory types: Sensors.""" from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, HumiditySensor, AirQualitySensor, CarbonDioxideSensor, - LightSensor, BinarySensor, BINARY_SENSOR_SERVICE_MAP) + AirQualitySensor, BinarySensor, CarbonDioxideSensor, HumiditySensor, + LightSensor, TemperatureSensor, BINARY_SENSOR_SERVICE_MAP) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, STATE_UNKNOWN, STATE_ON, - STATE_OFF, STATE_HOME, STATE_NOT_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_HOME, STATE_NOT_HOME, + STATE_OFF, STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) async def test_temperature(hass): diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 399a8bd84c8..5fc0b6ce1b9 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -3,7 +3,7 @@ import pytest from homeassistant.core import split_entity_id from homeassistant.components.homekit.type_switches import Switch -from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_OFF +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from tests.common import async_mock_service diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index bc5b3219cdf..337ad23ad05 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -4,15 +4,15 @@ from collections import namedtuple import pytest from homeassistant.components.climate import ( - DOMAIN, 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) + ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, + DOMAIN, STATE_AUTO, STATE_COOL, STATE_HEAT) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 42f81387960..0755e8f54d4 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -4,14 +4,14 @@ import voluptuous as vol from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( - show_setup_message, dismiss_setup_message, convert_to_float, - temperature_to_homekit, temperature_to_states, density_to_air_quality) + convert_to_float, density_to_air_quality, dismiss_setup_message, + show_setup_message, temperature_to_homekit, temperature_to_states) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( - DOMAIN, ATTR_MESSAGE, ATTR_NOTIFICATION_ID) + ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN) from homeassistant.const import ( - ATTR_CODE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) + ATTR_CODE, CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service From a183043d5dc19a6d4b5fd028e769672aad934313 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 21 May 2018 00:56:41 -0400 Subject: [PATCH 016/137] Add IlluminanceMeasurementSensor to ZHA (#14563) * add IlluminanceMeasurementSensor * address review comment * Fix whitespace error during merge --- homeassistant/components/sensor/zha.py | 19 ++++++++++++++++++- homeassistant/components/zha/const.py | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index abb4c651e78..72368bdb3ba 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -32,7 +32,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def make_sensor(discovery_info): """Create ZHA sensors factory.""" from zigpy.zcl.clusters.measurement import ( - RelativeHumidity, TemperatureMeasurement, PressureMeasurement + RelativeHumidity, TemperatureMeasurement, PressureMeasurement, + IlluminanceMeasurement ) from zigpy.zcl.clusters.smartenergy import Metering in_clusters = discovery_info['in_clusters'] @@ -42,6 +43,8 @@ def make_sensor(discovery_info): sensor = TemperatureSensor(**discovery_info) elif PressureMeasurement.cluster_id in in_clusters: sensor = PressureSensor(**discovery_info) + elif IlluminanceMeasurement.cluster_id in in_clusters: + sensor = IlluminanceMeasurementSensor(**discovery_info) elif Metering.cluster_id in in_clusters: sensor = MeteringSensor(**discovery_info) else: @@ -148,6 +151,20 @@ class PressureSensor(Sensor): return round(float(self._state)) +class IlluminanceMeasurementSensor(Sensor): + """ZHA lux sensor.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return 'lx' + + @property + def state(self): + """Return the state of the entity.""" + return self._state + + class MeteringSensor(Sensor): """ZHA Metering sensor.""" diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 71f0ea17490..087b19c6693 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -48,6 +48,7 @@ def populate_data(): zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', zcl.clusters.measurement.PressureMeasurement: 'sensor', + zcl.clusters.measurement.IlluminanceMeasurement: 'sensor', zcl.clusters.smartenergy.Metering: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', From 9791c6b21bd2b86bfc411b9c4a2b6a558014ec3b Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Sun, 20 May 2018 21:57:09 -0700 Subject: [PATCH 017/137] zha: Bump to zigpy-xbee 0.1.1 (#14566) --- homeassistant/components/zha/__init__.py | 7 ++++++- requirements_all.txt | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 3ea95ff1dd1..030e342847d 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -18,7 +18,7 @@ from homeassistant.util import slugify REQUIREMENTS = [ 'bellows==0.6.0', 'zigpy==0.1.0', - 'zigpy-xbee==0.1.0', + 'zigpy-xbee==0.1.1', ] DOMAIN = 'zha' @@ -151,6 +151,11 @@ class ApplicationListener: # Wait for device_initialized, instead pass + def raw_device_initialized(self, device): + """Handle a device initialization without quirks loaded.""" + # Wait for device_initialized, instead + pass + def device_initialized(self, device): """Handle device joined and basic information discovered.""" self._hass.async_add_job(self.async_device_initialized(device, True)) diff --git a/requirements_all.txt b/requirements_all.txt index 7d4b4bb3f41..80fe842b50f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1388,7 +1388,7 @@ zeroconf==0.20.0 ziggo-mediabox-xl==1.0.0 # homeassistant.components.zha -zigpy-xbee==0.1.0 +zigpy-xbee==0.1.1 # homeassistant.components.zha zigpy==0.1.0 From 2ff61786bc85c81ecd5004f7c02fdc3fb2622310 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 May 2018 11:01:35 -0400 Subject: [PATCH 018/137] Update frontend to 20180521.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 d4700e5edd3..04e4e0dae48 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180519.0'] +REQUIREMENTS = ['home-assistant-frontend==20180521.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 80fe842b50f..5794f54b457 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180519.0 +home-assistant-frontend==20180521.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d4d91b1037..a12cba141a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180519.0 +home-assistant-frontend==20180521.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 6e941af9b21f47307033bbd1d78042f6e784d41f Mon Sep 17 00:00:00 2001 From: Marco Orovecchia Date: Mon, 21 May 2018 17:02:50 +0200 Subject: [PATCH 019/137] fix nanoleaf aurora lights min and max temperature (#14571) * fixed nanoleaf aurora lights min and max temperature * review changes --- homeassistant/components/light/nanoleaf_aurora.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/light/nanoleaf_aurora.py b/homeassistant/components/light/nanoleaf_aurora.py index 8b0b7c053c8..6a0d3c36e9f 100644 --- a/homeassistant/components/light/nanoleaf_aurora.py +++ b/homeassistant/components/light/nanoleaf_aurora.py @@ -125,6 +125,16 @@ class AuroraLight(Light): """Return the list of supported effects.""" return self._effects_list + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return 154 + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return 833 + @property def name(self): """Return the display name of this light.""" From 23afdec76700edcba3d1743c4fcd9e789a08db70 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Mon, 21 May 2018 12:00:01 -0700 Subject: [PATCH 020/137] Fix ISY moisure sensors showing unknown until a leak is detected (#14496) * Fix ISY leak sensors always showing UNKNOWN until a leak is detected Added some logic that handles both moisture sensors and door/window sensors * Handle edge case of leak sensor status update after ISY reboot If a leak sensor is unknown, due to a recent reboot of the ISY, the status will get updated to dry upon the first heartbeat. This status update is the only way that a leak sensor's status changes without an accompanying Control event, so we need to watch for it. * Fixes from overnight testing State was checking the incorrect parameter, and wasn't calling schedule update * Remove leftover debug log line * Remove unnecessary pylint instruction * Remove access of protected property We can't cast _.status directly to a bool for some unknown reason (possibly with the VarEvents library), but casting to an int then bool does work. --- .../components/binary_sensor/isy994.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index fb86244acf3..09f1739cba7 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -117,8 +117,10 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): # pylint: disable=protected-access if _is_val_unknown(self._node.status._val): self._computed_state = None + self._status_was_unknown = True else: self._computed_state = bool(self._node.status._val) + self._status_was_unknown = False @asyncio.coroutine def async_added_to_hass(self) -> None: @@ -156,9 +158,13 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): # pylint: disable=protected-access if not _is_val_unknown(self._negative_node.status._val): # If the negative node has a value, it means the negative node is - # in use for this device. Therefore, we cannot determine the state - # of the sensor until we receive our first ON event. - self._computed_state = None + # in use for this device. Next we need to check to see if the + # negative and positive nodes disagree on the state (both ON or + # both OFF). + if self._negative_node.status._val == self._node.status._val: + # The states disagree, therefore we cannot determine the state + # of the sensor until we receive our first ON event. + self._computed_state = None def _negative_node_control_handler(self, event: object) -> None: """Handle an "On" control event from the "negative" node.""" @@ -189,14 +195,21 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): self.schedule_update_ha_state() self._heartbeat() - # pylint: disable=unused-argument def on_update(self, event: object) -> None: - """Ignore primary node status updates. + """Primary node status updates. - We listen directly to the Control events on all nodes for this - device. + We MOSTLY ignore these updates, as we listen directly to the Control + events on all nodes for this device. However, there is one edge case: + If a leak sensor is unknown, due to a recent reboot of the ISY, the + status will get updated to dry upon the first heartbeat. This status + update is the only way that a leak sensor's status changes without + an accompanying Control event, so we need to watch for it. """ - pass + if self._status_was_unknown and self._computed_state is None: + self._computed_state = bool(int(self._node.status)) + self._status_was_unknown = False + self.schedule_update_ha_state() + self._heartbeat() @property def value(self) -> object: From 0d9b3bea1029964d2e6c7e8204fc930a06423107 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 21 May 2018 19:46:20 -0400 Subject: [PATCH 021/137] Bump insteonplm version to fix device hanging (#14582) * Update inteonplm to 0.9.2 * Change to force Travis CI * Change to force Travis CI --- homeassistant/components/insteon_plm/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py index 246e84ec71f..b86f80cbee7 100644 --- a/homeassistant/components/insteon_plm/__init__.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.9.1'] +REQUIREMENTS = ['insteonplm==0.9.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5794f54b457..45cf3bc1e1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.9.1 +insteonplm==0.9.2 # homeassistant.components.verisure jsonpath==0.75 From 118c49ecaa37a4d0e29d4a4d91ebfd59be298a7e Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Tue, 22 May 2018 01:50:08 +0200 Subject: [PATCH 022/137] Update pyhomematic to 0.1.43 (#14583) * Update __init__.py * Update requirements_all.txt --- homeassistant/components/homematic/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index e0f0fafe5b5..29303b551e2 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.42'] +REQUIREMENTS = ['pyhomematic==0.1.43'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 45cf3bc1e1d..ef0aa86ee6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -805,7 +805,7 @@ pyhik==0.1.8 pyhiveapi==0.2.14 # homeassistant.components.homematic -pyhomematic==0.1.42 +pyhomematic==0.1.43 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.2.2 From 2753dd0c5e341fc2a3753430dc03a99ebd237ee5 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Tue, 22 May 2018 02:19:45 -0400 Subject: [PATCH 023/137] Removed attribute current_time from Raincloudy sensors to avoid being triggered by recorder component (#14584) --- homeassistant/components/raincloud.py | 1 - homeassistant/components/switch/raincloud.py | 1 - 2 files changed, 2 deletions(-) diff --git a/homeassistant/components/raincloud.py b/homeassistant/components/raincloud.py index 505c3a7b2b0..308a945e942 100644 --- a/homeassistant/components/raincloud.py +++ b/homeassistant/components/raincloud.py @@ -168,7 +168,6 @@ class RainCloudEntity(Entity): """Return the state attributes.""" return { ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - 'current_time': self.data.current_time, 'identifier': self.data.serial, } diff --git a/homeassistant/components/switch/raincloud.py b/homeassistant/components/switch/raincloud.py index 8a5c4347cf7..a4bac8fee1c 100644 --- a/homeassistant/components/switch/raincloud.py +++ b/homeassistant/components/switch/raincloud.py @@ -88,7 +88,6 @@ class RainCloudSwitch(RainCloudEntity, SwitchDevice): """Return the state attributes.""" return { ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - 'current_time': self.data.current_time, 'default_manual_timer': self._default_watering_timer, 'identifier': self.data.serial } From 72a1b7ae3fc5340b44c7978ae6d1551c47aef4d7 Mon Sep 17 00:00:00 2001 From: Julian Knauer Date: Tue, 22 May 2018 09:25:10 +0200 Subject: [PATCH 024/137] Lagute LW-12 Wifi LED control (#13307) * Added platform lw12wifi for Lagute LW-12 Wifi Lights Supported features: * RGB colors * Variable brightness * 29 effects * Changing transitions speed for animated effects * Added lw12wifi to the list of omitted files to test * Added lw12 module as new requirement for lw12wifi platform * Added configuration example docstring for platform lw12wifi * Updating code according to review in PR: * Removed unused imports: enum, socket. * Unused and not imported feature SUPPORT_FLASH was removed. * Unused import lw12 in setup_platform method removed. * Fixed indention for valuptuous. * Changed check if effect is None. * Removed personal debug output. * Blocking function are not async anymore. * Further improvements to satisfy PR. * Unused import asyncio removed. * Fixed: Return value and docstring no match up for `assumed_state`. * Check if the set effect is supported, otherwise revert to normal light. * Added describing missing docstrings to all functions. * Adopted code to work with HS color setting. * Syntactical change in comment. * Removed redefinition of DOMAIN. * Refactored lw12 controller setup: removed requirement for host and port in LW12Wifi class. * Rewritten supported feature setup to a more static expression. * Removed unused rgb_color property * Fixed typo in comment for set_light_option * Changed RGB option validation schema * Removed instance properties as config options * Removed optional settings to be more inline with code style. * Removed unused option from config example * Removal of unused import * Added property to disable state polling for this entity. * Raise an exception if an unknown effect was selected. * Fixed an issue with the check for known effects. * As we do not need to set a default, use simple accessing by key. * Log if an unknown effect was selected. * Added link to future documentation. --- .coveragerc | 1 + homeassistant/components/light/lw12wifi.py | 158 +++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 162 insertions(+) create mode 100644 homeassistant/components/light/lw12wifi.py diff --git a/.coveragerc b/.coveragerc index d361cf2ddad..a31af5f296c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -443,6 +443,7 @@ omit = homeassistant/components/light/lifx_legacy.py homeassistant/components/light/lifx.py homeassistant/components/light/limitlessled.py + homeassistant/components/light/lw12wifi.py homeassistant/components/light/mystrom.py homeassistant/components/light/nanoleaf_aurora.py homeassistant/components/light/osramlightify.py diff --git a/homeassistant/components/light/lw12wifi.py b/homeassistant/components/light/lw12wifi.py new file mode 100644 index 00000000000..f81d8368f98 --- /dev/null +++ b/homeassistant/components/light/lw12wifi.py @@ -0,0 +1,158 @@ +""" +Support for Lagute LW-12 WiFi LED Controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.lw12wifi/ +""" + +import logging + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, + Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, + SUPPORT_COLOR, SUPPORT_TRANSITION +) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PORT +) +import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util + + +REQUIREMENTS = ['lw12==0.9.2'] + +_LOGGER = logging.getLogger(__name__) + + +DEFAULT_NAME = 'LW-12 FC' +DEFAULT_PORT = 5000 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup LW-12 WiFi LED Controller platform.""" + import lw12 + + # Assign configuration variables. + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + # Add devices + lw12_light = lw12.LW12Controller(host, port) + add_devices([LW12WiFi(name, lw12_light)]) + + +class LW12WiFi(Light): + """LW-12 WiFi LED Controller.""" + + def __init__(self, name, lw12_light): + """Initialisation of LW-12 WiFi LED Controller. + + Args: + name: Friendly name for this platform to use. + lw12_light: Instance of the LW12 controller. + """ + self._light = lw12_light + self._name = name + self._state = None + self._effect = None + self._rgb_color = [255, 255, 255] + self._brightness = 255 + # Setup feature list + self._supported_features = SUPPORT_BRIGHTNESS | SUPPORT_EFFECT \ + | SUPPORT_COLOR | SUPPORT_TRANSITION + + @property + def name(self): + """Return the display name of the controlled light.""" + return self._name + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def hs_color(self): + """Read back the hue-saturation of the light.""" + return color_util.color_RGB_to_hs(*self._rgb_color) + + @property + def effect(self): + """Return current light effect.""" + if self._effect is None: + return None + return self._effect.replace('_', ' ').title() + + @property + def is_on(self): + """Return true if light is on.""" + return self._state + + @property + def supported_features(self): + """Return a list of supported features.""" + return self._supported_features + + @property + def effect_list(self): + """Return a list of available effects. + + Use the Enum element name for display. + """ + import lw12 + return [effect.name.replace('_', ' ').title() + for effect in lw12.LW12_EFFECT] + + @property + def assumed_state(self) -> bool: + """Return True if unable to access real state of the entity.""" + return True + + @property + def shoud_poll(self) -> bool: + """Return False to not poll the state of this entity.""" + return False + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + import lw12 + self._light.light_on() + if ATTR_HS_COLOR in kwargs: + self._rgb_color = color_util.color_hs_to_RGB( + *kwargs[ATTR_HS_COLOR]) + self._light.set_color(*self._rgb_color) + self._effect = None + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs.get(ATTR_BRIGHTNESS) + brightness = int(self._brightness / 255 * 100) + self._light.set_light_option(lw12.LW12_LIGHT.BRIGHTNESS, + brightness) + if ATTR_EFFECT in kwargs: + self._effect = kwargs[ATTR_EFFECT].replace(' ', '_').upper() + # Check if a known and supported effect was selected. + if self._effect in [eff.name for eff in lw12.LW12_EFFECT]: + # Selected effect is supported and will be applied. + self._light.set_effect(lw12.LW12_EFFECT[self._effect]) + else: + # Unknown effect was set, recover by disabling the effect + # mode and log an error. + _LOGGER.error("Unknown effect selected: %s", self._effect) + self._effect = None + if ATTR_TRANSITION in kwargs: + transition_speed = int(kwargs[ATTR_TRANSITION]) + self._light.set_light_option(lw12.LW12_LIGHT.FLASH, + transition_speed) + self._state = True + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._light.light_off() + self._state = False diff --git a/requirements_all.txt b/requirements_all.txt index ef0aa86ee6b..016e2f9ce2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,6 +514,9 @@ locationsharinglib==2.0.2 # homeassistant.components.sensor.luftdaten luftdaten==0.1.3 +# homeassistant.components.light.lw12wifi +lw12==0.9.2 + # homeassistant.components.sensor.lyft lyft_rides==0.2 From a2decdaaa32846ac06f53f93e939138de1fb659a Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 22 May 2018 17:34:02 +1000 Subject: [PATCH 025/137] NUT sensor enhancements (#14570) (Fixes #14324) * removed default value from required parameter; raising PlatformNotReady when connection to nut unavailable; output human-readable state name by default * removed superfluous sensor name part; showing human-readable form and raw value of current status in more info dialog * introduced a new virtual sensor type based on the raw status value but used to display a human-readable form of the status * renamed method * format string instead of concatenation * revert the change to the device state attributes - only output the human-readable status without the raw value --- homeassistant/components/sensor/nut.py | 28 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/nut.py b/homeassistant/components/sensor/nut.py index b8917080efc..bf440728a2e 100644 --- a/homeassistant/components/sensor/nut.py +++ b/homeassistant/components/sensor/nut.py @@ -14,6 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, TEMP_CELSIUS, CONF_RESOURCES, CONF_ALIAS, ATTR_STATE, STATE_UNKNOWN) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -26,10 +27,12 @@ DEFAULT_HOST = 'localhost' DEFAULT_PORT = 3493 KEY_STATUS = 'ups.status' +KEY_STATUS_DISPLAY = 'ups.status.display' MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) SENSOR_TYPES = { + 'ups.status.display': ['Status', '', 'mdi:information-outline'], 'ups.status': ['Status Data', '', 'mdi:information-outline'], 'ups.alarm': ['Alarms', '', 'mdi:alarm'], 'ups.time': ['Internal Time', '', 'mdi:calendar-clock'], @@ -130,7 +133,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ALIAS): cv.string, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, - vol.Required(CONF_RESOURCES, default=[]): + vol.Required(CONF_RESOURCES): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), }) @@ -148,7 +151,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if data.status is None: _LOGGER.error("NUT Sensor has no data, unable to setup") - return False + raise PlatformNotReady _LOGGER.debug('NUT Sensors Available: %s', data.status) @@ -157,7 +160,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for resource in config[CONF_RESOURCES]: sensor_type = resource.lower() - if sensor_type in data.status: + # Display status is a special case that falls back to the status value + # of the UPS instead. + if sensor_type in data.status or (sensor_type == KEY_STATUS_DISPLAY + and KEY_STATUS in data.status): entities.append(NUTSensor(name, data, sensor_type)) else: _LOGGER.warning( @@ -169,7 +175,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): except data.pynuterror as err: _LOGGER.error("Failure while testing NUT status retrieval. " "Cannot continue setup: %s", err) - return False + raise PlatformNotReady add_entities(entities, True) @@ -209,11 +215,11 @@ class NUTSensor(Entity): def device_state_attributes(self): """Return the sensor attributes.""" attr = dict() - attr[ATTR_STATE] = self.opp_state() + attr[ATTR_STATE] = self.display_state() return attr - def opp_state(self): - """Return UPS operating state.""" + def display_state(self): + """Return UPS display state.""" if self._data.status is None: return STATE_TYPES['OFF'] else: @@ -230,7 +236,11 @@ class NUTSensor(Entity): self._state = None return - if self.type not in self._data.status: + # In case of the display status sensor, keep a human-readable form + # as the sensor state. + if self.type == KEY_STATUS_DISPLAY: + self._state = self.display_state() + elif self.type not in self._data.status: self._state = None else: self._state = self._data.status[self.type] @@ -288,5 +298,5 @@ class PyNUTData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self, **kwargs): - """Fetch the latest status from APCUPSd.""" + """Fetch the latest status from NUT.""" self._status = self._get_status() From a2f9fdf3394542eeb7e4007ca49d7d707108e6ad Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 22 May 2018 10:06:14 +0200 Subject: [PATCH 026/137] Add new transmission sensor types (#14530) --- .../components/sensor/transmission.py | 104 +++++++++++------- 1 file changed, 63 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/sensor/transmission.py index 678d9afb81d..4dac411d224 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/sensor/transmission.py @@ -4,23 +4,23 @@ Support for monitoring the Transmission BitTorrent client API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.transmission/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, CONF_PORT, - CONF_MONITORED_VARIABLES, STATE_IDLE) + CONF_HOST, CONF_MONITORED_VARIABLES, CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_USERNAME, STATE_IDLE) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +from homeassistant.exceptions import PlatformNotReady REQUIREMENTS = ['transmissionrpc==0.11'] _LOGGER = logging.getLogger(__name__) -_THROTTLED_REFRESH = None DEFAULT_NAME = 'Transmission' DEFAULT_PORT = 9091 @@ -29,12 +29,16 @@ SENSOR_TYPES = { 'active_torrents': ['Active Torrents', None], 'current_status': ['Status', None], 'download_speed': ['Down Speed', 'MB/s'], + 'paused_torrents': ['Paused Torrents', None], + 'total_torrents': ['Total Torrents', None], 'upload_speed': ['Up Speed', 'MB/s'], } +SCAN_INTERVAL = timedelta(minutes=2) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=[]): + vol.Optional(CONF_MONITORED_VARIABLES, default=['torrents']): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, @@ -43,7 +47,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Transmission sensors.""" import transmissionrpc @@ -56,39 +59,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None): port = config.get(CONF_PORT) try: - transmission_api = transmissionrpc.Client( + transmission = transmissionrpc.Client( host, port=port, user=username, password=password) - transmission_api.session_stats() + transmission_api = TransmissionData(transmission) except TransmissionError as error: - _LOGGER.error( - "Connection to Transmission API failed on %s:%s with message %s", - host, port, error.original - ) - return False + if str(error).find("401: Unauthorized"): + _LOGGER.error("Credentials for Transmission client are not valid") + return - # pylint: disable=global-statement - global _THROTTLED_REFRESH - _THROTTLED_REFRESH = Throttle(timedelta(seconds=1))( - transmission_api.session_stats) + _LOGGER.warning( + "Unable to connect to Transmission client: %s:%s", host, port) + raise PlatformNotReady dev = [] for variable in config[CONF_MONITORED_VARIABLES]: dev.append(TransmissionSensor(variable, transmission_api, name)) - add_devices(dev) + add_devices(dev, True) class TransmissionSensor(Entity): """Representation of a Transmission sensor.""" - def __init__(self, sensor_type, transmission_client, client_name): + def __init__(self, sensor_type, transmission_api, client_name): """Initialize the sensor.""" self._name = SENSOR_TYPES[sensor_type][0] - self.tm_client = transmission_client - self.type = sensor_type - self.client_name = client_name self._state = None + self._transmission_api = transmission_api self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._data = None + self.client_name = client_name + self.type = sensor_type @property def name(self): @@ -105,25 +106,20 @@ class TransmissionSensor(Entity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - # pylint: disable=no-self-use - def refresh_transmission_data(self): - """Call the throttled Transmission refresh method.""" - from transmissionrpc.error import TransmissionError - - if _THROTTLED_REFRESH is not None: - try: - _THROTTLED_REFRESH() - except TransmissionError: - _LOGGER.error("Connection to Transmission API failed") + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self._transmission_api.available def update(self): """Get the latest data from Transmission and updates the state.""" - self.refresh_transmission_data() + self._transmission_api.update() + self._data = self._transmission_api.data if self.type == 'current_status': - if self.tm_client.session: - upload = self.tm_client.session.uploadSpeed - download = self.tm_client.session.downloadSpeed + if self._data: + upload = self._data.uploadSpeed + download = self._data.downloadSpeed if upload > 0 and download > 0: self._state = 'Up/Down' elif upload > 0 and download == 0: @@ -135,14 +131,40 @@ class TransmissionSensor(Entity): else: self._state = None - if self.tm_client.session: + if self._data: if self.type == 'download_speed': - mb_spd = float(self.tm_client.session.downloadSpeed) + mb_spd = float(self._data.downloadSpeed) mb_spd = mb_spd / 1024 / 1024 self._state = round(mb_spd, 2 if mb_spd < 0.1 else 1) elif self.type == 'upload_speed': - mb_spd = float(self.tm_client.session.uploadSpeed) + mb_spd = float(self._data.uploadSpeed) mb_spd = mb_spd / 1024 / 1024 self._state = round(mb_spd, 2 if mb_spd < 0.1 else 1) elif self.type == 'active_torrents': - self._state = self.tm_client.session.activeTorrentCount + self._state = self._data.activeTorrentCount + elif self.type == 'paused_torrents': + self._state = self._data.pausedTorrentCount + elif self.type == 'total_torrents': + self._state = self._data.torrentCount + + +class TransmissionData(object): + """Get the latest data and update the states.""" + + def __init__(self, api): + """Initialize the Transmission data object.""" + self.data = None + self.available = True + self._api = api + + @Throttle(SCAN_INTERVAL) + def update(self): + """Get the latest data from Transmission instance.""" + from transmissionrpc.error import TransmissionError + + try: + self.data = self._api.session_stats() + self.available = True + except TransmissionError: + self.available = False + _LOGGER.error("Unable to connect to Transmission client") From 82770faad71c8d2a34d427fe864686f8b213b3f0 Mon Sep 17 00:00:00 2001 From: SchumyHao Date: Tue, 22 May 2018 16:40:11 +0800 Subject: [PATCH 027/137] Add Xiaomi Aqara Lock support (#14419) --- homeassistant/components/lock/xiaomi_aqara.py | 92 +++++++++++++++++++ homeassistant/components/xiaomi_aqara.py | 3 +- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lock/xiaomi_aqara.py diff --git a/homeassistant/components/lock/xiaomi_aqara.py b/homeassistant/components/lock/xiaomi_aqara.py new file mode 100644 index 00000000000..9b084a2bc55 --- /dev/null +++ b/homeassistant/components/lock/xiaomi_aqara.py @@ -0,0 +1,92 @@ +""" +Support for Xiaomi Aqara Lock. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.xiaomi_aqara/ +""" +import logging +from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, + XiaomiDevice) +from homeassistant.components.lock import LockDevice +from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) +from homeassistant.helpers.event import async_call_later +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + +FINGER_KEY = 'fing_verified' +PASSWORD_KEY = 'psw_verified' +CARD_KEY = 'card_verified' +VERIFIED_WRONG_KEY = 'verified_wrong' + +ATTR_VERIFIED_WRONG_TIMES = 'verified_wrong_times' + +UNLOCK_MAINTAIN_TIME = 5 + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Perform the setup for Xiaomi devices.""" + devices = [] + + for gateway in hass.data[PY_XIAOMI_GATEWAY].gateways.values(): + for device in gateway.devices['lock']: + model = device['model'] + if model == 'lock.aq1': + devices.append(XiaomiAqaraLock(device, 'Lock', gateway)) + async_add_devices(devices) + + +class XiaomiAqaraLock(LockDevice, XiaomiDevice): + """Representation of a XiaomiAqaraLock.""" + + def __init__(self, device, name, xiaomi_hub): + """Initialize the XiaomiAqaraLock.""" + self._changed_by = 0 + self._verified_wrong_times = 0 + + super().__init__(device, name, xiaomi_hub) + + @property + def is_locked(self) -> bool: + """Return true if lock is locked.""" + if self._state is not None: + return self._state == STATE_LOCKED + + @property + def changed_by(self) -> int: + """Last change triggered by.""" + return self._changed_by + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes.""" + attributes = { + ATTR_VERIFIED_WRONG_TIMES: self._verified_wrong_times, + } + return attributes + + @callback + def clear_unlock_state(self, _): + """Clear unlock state automatically.""" + self._state = STATE_LOCKED + self.async_schedule_update_ha_state() + + def parse_data(self, data, raw_data): + """Parse data sent by gateway.""" + value = data.get(VERIFIED_WRONG_KEY) + if value is not None: + self._verified_wrong_times = int(value) + return True + + for key in (FINGER_KEY, PASSWORD_KEY, CARD_KEY): + value = data.get(key) + if value is not None: + self._changed_by = int(value) + self._verified_wrong_times = 0 + self._state = STATE_UNLOCKED + async_call_later(self.hass, UNLOCK_MAINTAIN_TIME, + self.clear_unlock_state) + return True + + return False diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 27bd496a3f0..ae3a4e0be72 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -139,7 +139,8 @@ def setup(hass, config): xiaomi.listen() _LOGGER.debug("Gateways discovered. Listening for broadcasts") - for component in ['binary_sensor', 'sensor', 'switch', 'light', 'cover']: + for component in ['binary_sensor', 'sensor', 'switch', 'light', 'cover', + 'lock']: discovery.load_platform(hass, component, DOMAIN, {}, config) def stop_xiaomi(event): From ad4994220124e5823e4924f275a6728b08f99800 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 23 May 2018 16:47:58 +0200 Subject: [PATCH 028/137] Upgrade TwitterAPI to 2.5.3 (#14596) --- homeassistant/components/notify/twitter.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index 9489e05cfa5..f81a83325ce 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.helpers.event import async_track_point_in_time -REQUIREMENTS = ['TwitterAPI==2.5.0'] +REQUIREMENTS = ['TwitterAPI==2.5.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 016e2f9ce2d..0879c539ea8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -61,7 +61,7 @@ SoCo==0.14 TravisPy==0.3.5 # homeassistant.components.notify.twitter -TwitterAPI==2.5.0 +TwitterAPI==2.5.3 # homeassistant.components.sensor.waze_travel_time WazeRouteCalculator==0.5 From c13ebacce16bf49c0d5117b56c44ab99bc228bce Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 23 May 2018 14:36:51 -0400 Subject: [PATCH 029/137] Remove nma component (#14594) * Remove nma component * Update .coveragerc --- .coveragerc | 1 - homeassistant/components/notify/nma.py | 65 -------------------------- 2 files changed, 66 deletions(-) delete mode 100644 homeassistant/components/notify/nma.py diff --git a/.coveragerc b/.coveragerc index a31af5f296c..eb73cb66c30 100644 --- a/.coveragerc +++ b/.coveragerc @@ -534,7 +534,6 @@ omit = homeassistant/components/notify/message_bird.py homeassistant/components/notify/mycroft.py homeassistant/components/notify/nfandroidtv.py - homeassistant/components/notify/nma.py homeassistant/components/notify/prowl.py homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushetta.py diff --git a/homeassistant/components/notify/nma.py b/homeassistant/components/notify/nma.py deleted file mode 100644 index e81dc457a81..00000000000 --- a/homeassistant/components/notify/nma.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -NMA (Notify My Android) notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.nma/ -""" -import logging -import xml.etree.ElementTree as ET - -import requests -import voluptuous as vol - -from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) -_RESOURCE = 'https://www.notifymyandroid.com/publicapi/' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, -}) - - -def get_service(hass, config, discovery_info=None): - """Get the NMA notification service.""" - parameters = { - 'apikey': config[CONF_API_KEY], - } - response = requests.get( - '{}{}'.format(_RESOURCE, 'verify'), params=parameters, timeout=5) - tree = ET.fromstring(response.content) - - if tree[0].tag == 'error': - _LOGGER.error("Wrong API key supplied: %s", tree[0].text) - return None - - return NmaNotificationService(config[CONF_API_KEY]) - - -class NmaNotificationService(BaseNotificationService): - """Implement the notification service for NMA.""" - - def __init__(self, api_key): - """Initialize the service.""" - self._api_key = api_key - - def send_message(self, message="", **kwargs): - """Send a message to a user.""" - data = { - 'apikey': self._api_key, - 'application': 'home-assistant', - 'event': kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), - 'description': message, - 'priority': 0, - } - - response = requests.get( - '{}{}'.format(_RESOURCE, 'notify'), params=data, timeout=5) - tree = ET.fromstring(response.content) - - if tree[0].tag == 'error': - _LOGGER.exception( - "Unable to perform request. Error: %s", tree[0].text) From 349823444834d145cc055a76ce578bb81e3f9555 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 23 May 2018 12:40:33 -0700 Subject: [PATCH 030/137] Add Nest away binary sensor and eta sensor (#14406) --- .../components/binary_sensor/nest.py | 24 ++++++++++++-- homeassistant/components/nest.py | 25 ++++++++++---- homeassistant/components/sensor/nest.py | 33 ++++++++++++++----- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 4089f3a2eaf..2a1732cd9f0 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -29,6 +29,16 @@ CAMERA_BINARY_TYPES = [ 'person_detected', ] +STRUCTURE_BINARY_TYPES = [ + 'away', + # 'security_state', # wait for pending python-nest update +] + +STRUCTURE_BINARY_STATE_MAP = { + 'away': {'away': True, 'home': False}, + 'security_state': {'deter': True, 'ok': False}, +} + _BINARY_TYPES_DEPRECATED = [ 'hvac_ac_state', 'hvac_aux_heater_state', @@ -41,7 +51,7 @@ _BINARY_TYPES_DEPRECATED = [ ] _VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \ - + CAMERA_BINARY_TYPES + + CAMERA_BINARY_TYPES + STRUCTURE_BINARY_TYPES _LOGGER = logging.getLogger(__name__) @@ -68,6 +78,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error(wstr) sensors = [] + for structure in nest.structures(): + sensors += [NestBinarySensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_BINARY_TYPES] device_chain = chain(nest.thermostats(), nest.smoke_co_alarms(), nest.cameras()) @@ -88,7 +102,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors += [NestActivityZoneSensor(structure, device, activity_zone)] - add_devices(sensors, True) @@ -102,7 +115,12 @@ class NestBinarySensor(NestSensor, BinarySensorDevice): def update(self): """Retrieve latest state.""" - self._state = bool(getattr(self.device, self.variable)) + value = getattr(self.device, self.variable) + if self.variable in STRUCTURE_BINARY_TYPES: + self._state = bool(STRUCTURE_BINARY_STATE_MAP + [self.variable][value]) + else: + self._state = bool(value) class NestActivityZoneSensor(NestBinarySensor): diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index e7d2ba90438..2500755d495 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -168,6 +168,19 @@ class NestDevice(object): self.local_structure = conf[CONF_STRUCTURE] _LOGGER.debug("Structures to include: %s", self.local_structure) + def structures(self): + """Generate a list of structures.""" + try: + for structure in self.nest.structures: + if structure.name in self.local_structure: + yield structure + else: + _LOGGER.debug("Ignoring structure %s, not in %s", + structure.name, self.local_structure) + except socket.error: + _LOGGER.error( + "Connection error logging into the nest web service.") + def thermostats(self): """Generate a list of thermostats and their location.""" try: @@ -188,10 +201,10 @@ class NestDevice(object): for structure in self.nest.structures: if structure.name in self.local_structure: for device in structure.smoke_co_alarms: - yield(structure, device) + yield (structure, device) else: - _LOGGER.info("Ignoring structure %s, not in %s", - structure.name, self.local_structure) + _LOGGER.debug("Ignoring structure %s, not in %s", + structure.name, self.local_structure) except socket.error: _LOGGER.error( "Connection error logging into the nest web service.") @@ -202,10 +215,10 @@ class NestDevice(object): for structure in self.nest.structures: if structure.name in self.local_structure: for device in structure.cameras: - yield(structure, device) + yield (structure, device) else: - _LOGGER.info("Ignoring structure %s, not in %s", - structure.name, self.local_structure) + _LOGGER.debug("Ignoring structure %s, not in %s", + structure.name, self.local_structure) except socket.error: _LOGGER.error( "Connection error logging into the nest web service.") diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 9ce50dc61e5..0de2e2e0cdb 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -36,10 +36,15 @@ PROTECT_VARS_DEPRECATED = ['battery_level'] SENSOR_TEMP_TYPES = ['temperature', 'target'] +STRUCTURE_SENSOR_TYPES = ['eta'] + +VARIABLE_NAME_MAPPING = {'eta': 'eta_begin', 'operation_mode': 'mode'} + _SENSOR_TYPES_DEPRECATED = SENSOR_TYPES_DEPRECATED \ + list(DEPRECATED_WEATHER_VARS.keys()) + PROTECT_VARS_DEPRECATED -_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS +_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS \ + + STRUCTURE_SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -73,6 +78,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error(wstr) all_sensors = [] + for structure in nest.structures(): + all_sensors += [NestBasicSensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_SENSOR_TYPES] for structure, device in chain(nest.thermostats(), nest.smoke_co_alarms()): sensors = [NestBasicSensor(structure, device, variable) for variable in conditions @@ -94,13 +103,20 @@ class NestSensor(Entity): def __init__(self, structure, device, variable): """Initialize the sensor.""" self.structure = structure - self.device = device self.variable = variable - # device specific - self._location = self.device.where - self._name = "{} {}".format(self.device.name_long, - self.variable.replace("_", " ")) + if device is not None: + # device specific + self.device = device + self._location = self.device.where + self._name = "{} {}".format(self.device.name_long, + self.variable.replace('_', ' ')) + else: + # structure only + self.device = structure + self._name = "{} {}".format(self.structure.name, + self.variable.replace('_', ' ')) + self._state = None self._unit = None @@ -127,8 +143,9 @@ class NestBasicSensor(NestSensor): """Retrieve latest state.""" self._unit = SENSOR_UNITS.get(self.variable, None) - if self.variable == 'operation_mode': - self._state = getattr(self.device, "mode") + if self.variable in VARIABLE_NAME_MAPPING: + self._state = getattr(self.device, + VARIABLE_NAME_MAPPING[self.variable]) else: self._state = getattr(self.device, self.variable) From 5205354cb7dd947de219b322f0e12880dee55dc5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 23 May 2018 23:58:35 -0600 Subject: [PATCH 031/137] Adds a device class of 'garage' to MyQ covers (#14602) --- homeassistant/components/cover/myq.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index f07d3849fae..1e2ec43181c 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -69,6 +69,11 @@ class MyQDevice(CoverDevice): self._name = device['name'] self._status = STATE_CLOSED + @property + def device_class(self): + """Define this cover as a garage door.""" + return 'garage' + @property def should_poll(self): """Poll for state.""" From 36da82aa8dab846f3aaec966909d17d975e47ef2 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Thu, 24 May 2018 03:25:27 -0400 Subject: [PATCH 032/137] Add Iperf3 client sensor (#14213) --- .coveragerc | 1 + Dockerfile | 1 + homeassistant/components/sensor/iperf3.py | 178 +++++++++++++++++++++ requirements_all.txt | 3 + virtualization/Docker/Dockerfile.dev | 1 + virtualization/Docker/scripts/iperf3 | 11 ++ virtualization/Docker/setup_docker_prereqs | 5 + 7 files changed, 200 insertions(+) create mode 100644 homeassistant/components/sensor/iperf3.py create mode 100755 virtualization/Docker/scripts/iperf3 diff --git a/.coveragerc b/.coveragerc index eb73cb66c30..3ccfdeb3569 100644 --- a/.coveragerc +++ b/.coveragerc @@ -619,6 +619,7 @@ omit = homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/influxdb.py + homeassistant/components/sensor/iperf3.py homeassistant/components/sensor/irish_rail_transport.py homeassistant/components/sensor/kwb.py homeassistant/components/sensor/lacrosse.py diff --git a/Dockerfile b/Dockerfile index 5081b4ba721..75d9e9eb716 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no #ENV INSTALL_SSOCR no +#ENV INSTALL_IPERF3 no VOLUME /config diff --git a/homeassistant/components/sensor/iperf3.py b/homeassistant/components/sensor/iperf3.py new file mode 100644 index 00000000000..1a209faf17f --- /dev/null +++ b/homeassistant/components/sensor/iperf3.py @@ -0,0 +1,178 @@ +""" +Support for Iperf3 network measurement tool. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.iperf3/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_ENTITY_ID, CONF_MONITORED_CONDITIONS, + CONF_HOST, CONF_PORT) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['iperf3==0.1.10'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_PROTOCOL = 'Protocol' +ATTR_REMOTE_HOST = 'Remote Server' +ATTR_REMOTE_PORT = 'Remote Port' +ATTR_VERSION = 'Version' + +CONF_ATTRIBUTION = 'Data retrieved using Iperf3' +CONF_DURATION = 'duration' + +DEFAULT_DURATION = 10 +DEFAULT_PORT = 5201 + +IPERF3_DATA = 'iperf3' + +SCAN_INTERVAL = timedelta(minutes=30) + +SERVICE_NAME = 'iperf3_update' + +ICON = 'mdi:speedometer' + +SENSOR_TYPES = { + 'download': ['Download', 'Mbit/s'], + 'upload': ['Upload', 'Mbit/s'], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Range(5, 10), +}) + + +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Iperf3 sensor.""" + if hass.data.get(IPERF3_DATA) is None: + hass.data[IPERF3_DATA] = {} + hass.data[IPERF3_DATA]['sensors'] = [] + + dev = [] + for sensor in config[CONF_MONITORED_CONDITIONS]: + dev.append( + Iperf3Sensor(config[CONF_HOST], + config[CONF_PORT], + config[CONF_DURATION], + sensor)) + + hass.data[IPERF3_DATA]['sensors'].extend(dev) + add_devices(dev) + + def _service_handler(service): + """Update service for manual updates.""" + entity_id = service.data.get('entity_id') + all_iperf3_sensors = hass.data[IPERF3_DATA]['sensors'] + + for sensor in all_iperf3_sensors: + if entity_id is not None: + if sensor.entity_id == entity_id: + sensor.update() + sensor.schedule_update_ha_state() + break + else: + sensor.update() + sensor.schedule_update_ha_state() + + for sensor in dev: + hass.services.register(DOMAIN, SERVICE_NAME, _service_handler, + schema=SERVICE_SCHEMA) + + +class Iperf3Sensor(Entity): + """A Iperf3 sensor implementation.""" + + def __init__(self, server, port, duration, sensor_type): + """Initialize the sensor.""" + self._attrs = { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + } + self._name = \ + "{} {}".format(SENSOR_TYPES[sensor_type][0], server) + self._state = None + self._sensor_type = sensor_type + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._port = port + self._server = server + self._duration = duration + self.result = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self.result is not None: + self._attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + self._attrs[ATTR_PROTOCOL] = self.result.protocol + self._attrs[ATTR_REMOTE_HOST] = self.result.remote_host + self._attrs[ATTR_REMOTE_PORT] = self.result.remote_port + self._attrs[ATTR_VERSION] = self.result.version + return self._attrs + + def update(self): + """Get the latest data and update the states.""" + import iperf3 + client = iperf3.Client() + client.duration = self._duration + client.server_hostname = self._server + client.port = self._port + client.verbose = False + + # when testing download bandwith, reverse must be True + if self._sensor_type == 'download': + client.reverse = True + + try: + self.result = client.run() + except (OSError, AttributeError) as error: + self.result = None + _LOGGER.error("Iperf3 sensor error: %s", error) + return + + if self.result is not None and \ + hasattr(self.result, 'error') and \ + self.result.error is not None: + _LOGGER.error("Iperf3 sensor error: %s", self.result.error) + self.result = None + return + + if self._sensor_type == 'download': + self._state = round(self.result.received_Mbps, 2) + + elif self._sensor_type == 'upload': + self._state = round(self.result.sent_Mbps, 2) + + @property + def icon(self): + """Return icon.""" + return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 0879c539ea8..7a40a1aa48e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -451,6 +451,9 @@ insteonlocal==0.53 # homeassistant.components.insteon_plm insteonplm==0.9.2 +# homeassistant.components.sensor.iperf3 +iperf3==0.1.10 + # homeassistant.components.verisure jsonpath==0.75 diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 06676140702..d0599c2e74c 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -13,6 +13,7 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_PHANTOMJS no #ENV INSTALL_COAP no #ENV INSTALL_SSOCR no +#ENV INSTALL_IPERF3 no VOLUME /config diff --git a/virtualization/Docker/scripts/iperf3 b/virtualization/Docker/scripts/iperf3 new file mode 100755 index 00000000000..2d9d5a33761 --- /dev/null +++ b/virtualization/Docker/scripts/iperf3 @@ -0,0 +1,11 @@ +#!/bin/bash +# Sets up iperf3. + +# Stop on errors +set -e + +PACKAGES=( + iperf3 +) + +apt-get install -y --no-install-recommends ${PACKAGES[@]} diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 302dfba2e1d..3bb4136c991 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -10,6 +10,7 @@ INSTALL_FFMPEG="${INSTALL_FFMPEG:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" +INSTALL_IPERF3="${INSTALL_IPERF3:-yes}" # Required debian packages for running hass or components PACKAGES=( @@ -64,6 +65,10 @@ if [ "$INSTALL_SSOCR" == "yes" ]; then virtualization/Docker/scripts/ssocr fi +if [ "$INSTALL_IPERF3" == "yes" ]; then + virtualization/Docker/scripts/iperf3 +fi + # Remove packages apt-get remove -y --purge ${PACKAGES_DEV[@]} apt-get -y --purge autoremove From 3a487e54a2ee7e21438d71d2c2deb86c169fc270 Mon Sep 17 00:00:00 2001 From: Robert Beal Date: Thu, 24 May 2018 16:16:35 +0100 Subject: [PATCH 033/137] Upgrade linode-api to 4.1.9b1 (#13863) (#14610) --- homeassistant/components/linode.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/linode.py b/homeassistant/components/linode.py index 9e87c002482..962e30774b8 100644 --- a/homeassistant/components/linode.py +++ b/homeassistant/components/linode.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['linode-api==4.1.4b2'] +REQUIREMENTS = ['linode-api==4.1.9b1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7a40a1aa48e..42e512f08e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -502,7 +502,7 @@ lightify==1.0.6.1 limitlessled==1.1.0 # homeassistant.components.linode -linode-api==4.1.4b2 +linode-api==4.1.9b1 # homeassistant.components.media_player.liveboxplaytv liveboxplaytv==2.0.2 From 4fb4838bdece49d0575a59d1efa95218abb68a4b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 May 2018 13:08:12 -0400 Subject: [PATCH 034/137] Update frontend to 20180524.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 04e4e0dae48..8ee6ce549a4 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180521.0'] +REQUIREMENTS = ['home-assistant-frontend==20180524.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 42e512f08e2..c755f15f35f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180521.0 +home-assistant-frontend==20180524.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a12cba141a1..275a1974104 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180521.0 +home-assistant-frontend==20180524.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From fa9b9105a813285dce9cdb12638e83188285b3bb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 May 2018 14:24:14 -0400 Subject: [PATCH 035/137] Fix hue discovery popping up (#14614) * Fix hue discovery popping up * Fix result * Fix tests --- homeassistant/auth.py | 3 +++ homeassistant/config_entries.py | 15 +++++++++------ homeassistant/data_entry_flow.py | 8 ++++---- tests/test_config_entries.py | 20 ++++++++++++++++++++ tests/test_data_entry_flow.py | 3 ++- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 7c01776b7b1..5e434b74ca8 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -347,6 +347,9 @@ class AuthManager: async def _async_finish_login_flow(self, result): """Result of a credential login flow.""" + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return None + auth_provider = self._providers[result['handler']] return await auth_provider.async_get_or_create_credentials( result['data']) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1350cd7d76a..8a73e424fb5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -347,6 +347,15 @@ class ConfigEntries: async def _async_finish_flow(self, result): """Finish a config flow and add an entry.""" + # If no discovery config entries in progress, remove notification. + if not any(ent['source'] in DISCOVERY_SOURCES for ent + in self.hass.config_entries.flow.async_progress()): + self.hass.components.persistent_notification.async_dismiss( + DISCOVERY_NOTIFICATION_ID) + + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return None + entry = ConfigEntry( version=result['version'], domain=result['handler'], @@ -370,12 +379,6 @@ class ConfigEntries: if result['source'] not in DISCOVERY_SOURCES: return entry - # If no discovery config entries in progress, remove notification. - if not any(ent['source'] in DISCOVERY_SOURCES for ent - in self.hass.config_entries.flow.async_progress()): - self.hass.components.persistent_notification.async_dismiss( - DISCOVERY_NOTIFICATION_ID) - return entry async def _async_create_flow(self, handler, *, source, data): diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index e9580aba273..5095297e795 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -110,11 +110,11 @@ class FlowManager: # Abort and Success results both finish the flow self._progress.pop(flow.flow_id) - if result['type'] == RESULT_TYPE_ABORT: - return result - # We pass a copy of the result because we're mutating our version - result['result'] = await self._async_finish_flow(dict(result)) + entry = await self._async_finish_flow(dict(result)) + + if result['type'] == RESULT_TYPE_CREATE_ENTRY: + result['result'] = entry return result diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 1518706db55..84bd0771542 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -284,3 +284,23 @@ async def test_discovery_notification(hass): await hass.async_block_till_done() state = hass.states.get('persistent_notification.config_entry_discovery') assert state is None + + +async def test_discovery_notification_not_created(hass): + """Test that we not create a notification when discovery is aborted.""" + loader.set_component(hass, 'test', MockModule('test')) + await async_setup_component(hass, 'persistent_notification', {}) + + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + + async def async_step_discovery(self, user_input=None): + return self.async_abort(reason='test') + + with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): + await hass.config_entries.flow.async_init( + 'test', source=data_entry_flow.SOURCE_DISCOVERY) + + await hass.async_block_till_done() + state = hass.states.get('persistent_notification.config_entry_discovery') + assert state is None diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 6d3e41436c5..894fd4d7194 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -21,7 +21,8 @@ def manager(): return handler() async def async_add_entry(result): - entries.append(result) + if (result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY): + entries.append(result) manager = data_entry_flow.FlowManager( None, async_create_flow, async_add_entry) From 2cd127921a5c65e135fcfd63c1aa16c075dfe622 Mon Sep 17 00:00:00 2001 From: Gregory Benner Date: Fri, 25 May 2018 00:39:41 -0400 Subject: [PATCH 036/137] Update pyrainbird (#14617) --- homeassistant/components/rainbird.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rainbird.py b/homeassistant/components/rainbird.py index 76dda6fd366..bbce7f752af 100644 --- a/homeassistant/components/rainbird.py +++ b/homeassistant/components/rainbird.py @@ -11,7 +11,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import (CONF_HOST, CONF_PASSWORD) -REQUIREMENTS = ['pyrainbird==0.1.3'] +REQUIREMENTS = ['pyrainbird==0.1.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index c755f15f35f..d50fe829712 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ pypollencom==1.1.2 pyqwikswitch==0.8 # homeassistant.components.rainbird -pyrainbird==0.1.3 +pyrainbird==0.1.6 # homeassistant.components.sabnzbd pysabnzbd==1.0.1 From 6e5c541a001a27a2428dc779c9d41f8fbb5e78bb Mon Sep 17 00:00:00 2001 From: bastshoes Date: Fri, 25 May 2018 10:58:53 +0300 Subject: [PATCH 037/137] Add support container status for Glances on RPi3 (#14529) * Add support container status for Glances on RPi3 Glances on RPi3 return different container status. ``` "containers": [ { "Status": "Up 2 hours", "name": "HASS", "io": { "iow": 0, "time_since_update": 5.1789350509643555, "cumulative_ior": 94208, "ior": 0, "cumulative_iow": 4096 }, ``` This small PR adds support dealing with this differences. * Making line shorter * Fixing indentation * Fix lint error * Fix ident * Fix intend --- homeassistant/components/sensor/glances.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 3b6f3ddc99d..0de87bd17ea 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -161,7 +161,8 @@ class GlancesSensor(Entity): elif self.type == 'docker_active': count = 0 for container in value['docker']['containers']: - if container['Status'] == 'running': + if container['Status'] == 'running' or \ + 'Up' in container['Status']: count += 1 self._state = count elif self.type == 'docker_cpu_use': From d53a8c08238dd03b3574e873c8796eb0bc809011 Mon Sep 17 00:00:00 2001 From: Nik Klever Date: Fri, 25 May 2018 10:29:20 +0200 Subject: [PATCH 038/137] Adding illumination sensor (#14615) * Adding illumination sensor Adding Illumination sensor of 1wire device DS2438 (DEVICE_SENSOR type 26) according to [OWFS API](http://owfs.org/index.php?page=ds2438) * Correcting typo illumination -> illuminance --- homeassistant/components/sensor/onewire.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 8a07d3484d5..43105d54e38 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -28,7 +28,8 @@ DEVICE_SENSORS = {'10': {'temperature': 'temperature'}, '22': {'temperature': 'temperature'}, '26': {'temperature': 'temperature', 'humidity': 'humidity', - 'pressure': 'B1-R1-A/pressure'}, + 'pressure': 'B1-R1-A/pressure', + 'illuminance': 'S3-R1-A/illuminance'}, '28': {'temperature': 'temperature'}, '3B': {'temperature': 'temperature'}, '42': {'temperature': 'temperature'}} @@ -37,6 +38,7 @@ SENSOR_TYPES = { 'temperature': ['temperature', TEMP_CELSIUS], 'humidity': ['humidity', '%'], 'pressure': ['pressure', 'mb'], + 'illuminance': ['illuminance', 'lux'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ From a9f19a16ee3e2a625832ffda6a0520ff2035edc5 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Fri, 25 May 2018 05:37:20 -0400 Subject: [PATCH 039/137] Add HomeKit support for media players (#14446) --- homeassistant/components/homekit/__init__.py | 14 +- homeassistant/components/homekit/const.py | 6 + .../components/homekit/type_media_players.py | 142 ++++++++++++++++++ homeassistant/components/homekit/util.py | 43 +++++- .../homekit/test_get_accessories.py | 22 ++- .../homekit/test_type_media_players.py | 106 +++++++++++++ tests/components/homekit/test_util.py | 32 +++- 7 files changed, 354 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/homekit/type_media_players.py create mode 100644 tests/components/homekit/test_type_media_players.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 202f9694689..561301cdb6d 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -13,7 +13,7 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, + CONF_IP_ADDRESS, CONF_MODE, CONF_NAME, CONF_PORT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -25,7 +25,8 @@ from .const import ( CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START) -from .util import show_setup_message, validate_entity_config +from .util import ( + show_setup_message, validate_entity_config, validate_media_player_modes) TYPES = Registry() _LOGGER = logging.getLogger(__name__) @@ -125,6 +126,11 @@ def get_accessory(hass, state, aid, config): elif state.domain == 'lock': a_type = 'Lock' + elif state.domain == 'media_player': + validate_media_player_modes(state, config) + if config.get(CONF_MODE): + a_type = 'MediaPlayer' + elif state.domain == 'sensor': unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) device_class = state.attributes.get(ATTR_DEVICE_CLASS) @@ -208,8 +214,8 @@ class HomeKit(): # pylint: disable=unused-variable from . import ( # noqa F401 type_covers, type_fans, type_lights, type_locks, - type_security_systems, type_sensors, type_switches, - type_thermostats) + type_media_players, type_security_systems, type_sensors, + type_switches, type_thermostats) for state in self.hass.states.all(): self.add_bridge_accessory(state) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 21cad2d9cf7..f59ee5488ec 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -23,6 +23,12 @@ BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' MANUFACTURER = 'Home Assistant' +# #### Media Player Modes #### +ON_OFF = 'on_off' +PLAY_PAUSE = 'play_pause' +PLAY_STOP = 'play_stop' +TOGGLE_MUTE = 'toggle_mute' + # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py new file mode 100644 index 00000000000..563cd0cb25c --- /dev/null +++ b/homeassistant/components/homekit/type_media_players.py @@ -0,0 +1,142 @@ +"""Class to hold all media player accessories.""" +import logging + +from pyhap.const import CATEGORY_SWITCH + +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_MODE, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, + STATE_OFF, STATE_PLAYING, STATE_UNKNOWN) +from homeassistant.components.media_player import ( + ATTR_MEDIA_VOLUME_MUTED, DOMAIN) + +from . import TYPES +from .accessories import HomeAccessory +from .const import ( + CHAR_NAME, CHAR_ON, ON_OFF, PLAY_PAUSE, PLAY_STOP, SERV_SWITCH, + TOGGLE_MUTE) + +_LOGGER = logging.getLogger(__name__) + +MODE_FRIENDLY_NAME = {ON_OFF: 'Power', + PLAY_PAUSE: 'Play/Pause', + PLAY_STOP: 'Play/Stop', + TOGGLE_MUTE: 'Mute'} + + +@TYPES.register('MediaPlayer') +class MediaPlayer(HomeAccessory): + """Generate a Media Player accessory.""" + + def __init__(self, *args): + """Initialize a Switch accessory object.""" + super().__init__(*args, category=CATEGORY_SWITCH) + self._flag = {ON_OFF: False, PLAY_PAUSE: False, + PLAY_STOP: False, TOGGLE_MUTE: False} + self.chars = {ON_OFF: None, PLAY_PAUSE: None, + PLAY_STOP: None, TOGGLE_MUTE: None} + modes = self.config[CONF_MODE] + + if ON_OFF in modes: + serv_on_off = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_on_off.configure_char( + CHAR_NAME, value=self.generate_service_name(ON_OFF)) + self.chars[ON_OFF] = serv_on_off.configure_char( + CHAR_ON, value=False, setter_callback=self.set_on_off) + + if PLAY_PAUSE in modes: + serv_play_pause = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_play_pause.configure_char( + CHAR_NAME, value=self.generate_service_name(PLAY_PAUSE)) + self.chars[PLAY_PAUSE] = serv_play_pause.configure_char( + CHAR_ON, value=False, setter_callback=self.set_play_pause) + + if PLAY_STOP in modes: + serv_play_stop = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_play_stop.configure_char( + CHAR_NAME, value=self.generate_service_name(PLAY_STOP)) + self.chars[PLAY_STOP] = serv_play_stop.configure_char( + CHAR_ON, value=False, setter_callback=self.set_play_stop) + + if TOGGLE_MUTE in modes: + serv_toggle_mute = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_toggle_mute.configure_char( + CHAR_NAME, value=self.generate_service_name(TOGGLE_MUTE)) + self.chars[TOGGLE_MUTE] = serv_toggle_mute.configure_char( + CHAR_ON, value=False, setter_callback=self.set_toggle_mute) + + def generate_service_name(self, mode): + """Generate name for individual service.""" + return '{} {}'.format(self.display_name, MODE_FRIENDLY_NAME[mode]) + + def set_on_off(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "on_off" to %s', + self.entity_id, value) + self._flag[ON_OFF] = True + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) + + def set_play_pause(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "play_pause" to %s', + self.entity_id, value) + self._flag[PLAY_PAUSE] = True + service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) + + def set_play_stop(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "play_stop" to %s', + self.entity_id, value) + self._flag[PLAY_STOP] = True + service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) + + def set_toggle_mute(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "toggle_mute" to %s', + self.entity_id, value) + self._flag[TOGGLE_MUTE] = True + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_MEDIA_VOLUME_MUTED: value} + self.hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, params) + + def update_state(self, new_state): + """Update switch state after state changed.""" + current_state = new_state.state + + if self.chars[ON_OFF]: + hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN, 'None') + if not self._flag[ON_OFF]: + _LOGGER.debug('%s: Set current state for "on_off" to %s', + self.entity_id, hk_state) + self.chars[ON_OFF].set_value(hk_state) + self._flag[ON_OFF] = False + + if self.chars[PLAY_PAUSE]: + hk_state = current_state == STATE_PLAYING + if not self._flag[PLAY_PAUSE]: + _LOGGER.debug('%s: Set current state for "play_pause" to %s', + self.entity_id, hk_state) + self.chars[PLAY_PAUSE].set_value(hk_state) + self._flag[PLAY_PAUSE] = False + + if self.chars[PLAY_STOP]: + hk_state = current_state == STATE_PLAYING + if not self._flag[PLAY_STOP]: + _LOGGER.debug('%s: Set current state for "play_stop" to %s', + self.entity_id, hk_state) + self.chars[PLAY_STOP].set_value(hk_state) + self._flag[PLAY_STOP] = False + + if self.chars[TOGGLE_MUTE]: + current_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) + if not self._flag[TOGGLE_MUTE]: + _LOGGER.debug('%s: Set current state for "toggle_mute" to %s', + self.entity_id, current_state) + self.chars[TOGGLE_MUTE].set_value(current_state) + self._flag[TOGGLE_MUTE] = False diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 447257f9e8f..57ce562ce21 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -3,15 +3,21 @@ import logging import voluptuous as vol +from homeassistant.components.media_player import ( + SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_STOP, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE) from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE, CONF_NAME, TEMP_CELSIUS) + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_MODE, CONF_NAME, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util -from .const import HOMEKIT_NOTIFY_ID +from .const import ( + HOMEKIT_NOTIFY_ID, ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) _LOGGER = logging.getLogger(__name__) +MEDIA_PLAYER_MODES = (ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) + def validate_entity_config(values): """Validate config entry for CONF_ENTITY.""" @@ -34,10 +40,43 @@ def validate_entity_config(values): code = config.get(ATTR_CODE) params[ATTR_CODE] = cv.string(code) if code else None + if domain == 'media_player': + mode = config.get(CONF_MODE) + params[CONF_MODE] = cv.ensure_list(mode) + for key in params[CONF_MODE]: + if key not in MEDIA_PLAYER_MODES: + raise vol.Invalid( + 'Invalid mode: "{}", valid modes are: "{}".' + .format(key, MEDIA_PLAYER_MODES)) + entities[entity] = params return entities +def validate_media_player_modes(state, config): + """Validate modes for media playeres.""" + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + supported_modes = [] + if features & (SUPPORT_TURN_ON | SUPPORT_TURN_OFF): + supported_modes.append(ON_OFF) + if features & (SUPPORT_PLAY | SUPPORT_PAUSE): + supported_modes.append(PLAY_PAUSE) + if features & (SUPPORT_PLAY | SUPPORT_STOP): + supported_modes.append(PLAY_STOP) + if features & SUPPORT_VOLUME_MUTE: + supported_modes.append(TOGGLE_MUTE) + + if not config.get(CONF_MODE): + config[CONF_MODE] = supported_modes + return + + for mode in config[CONF_MODE]: + if mode not in supported_modes: + raise vol.Invalid('"{}" does not support mode: "{}".' + .format(state.entity_id, mode)) + + def show_setup_message(hass, pincode): """Display persistent notification with setup information.""" pin = pincode.decode() diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 25a0dd3f1cb..6f6d39e477a 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -2,15 +2,20 @@ from unittest.mock import patch, Mock import pytest +import voluptuous as vol from homeassistant.core import State from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.components.media_player import ( + SUPPORT_TURN_OFF, SUPPORT_TURN_ON) from homeassistant.components.homekit import get_accessory, TYPES +from homeassistant.components.homekit.const import ON_OFF from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, CONF_MODE, CONF_NAME, TEMP_CELSIUS, + TEMP_FAHRENHEIT) def test_not_supported(caplog): @@ -24,6 +29,18 @@ def test_not_supported(caplog): assert 'invalid aid' in caplog.records[0].msg +def test_not_supported_media_player(): + """Test if mode isn't supported and if no supported modes.""" + # selected mode for entity not supported + with pytest.raises(vol.Invalid): + entity_state = State('media_player.demo', 'on') + get_accessory(None, entity_state, 2, {CONF_MODE: [ON_OFF]}) + + # no supported modes for entity + entity_state = State('media_player.demo', 'on') + assert get_accessory(None, entity_state, 2, {}) is None + + @pytest.mark.parametrize('config, name', [ ({CONF_NAME: 'Customize Name'}, 'Customize Name'), ]) @@ -40,6 +57,9 @@ def test_customize_options(config, name): ('Fan', 'fan.test', 'on', {}, {}), ('Light', 'light.test', 'on', {}, {}), ('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}), + ('MediaPlayer', 'media_player.test', 'on', + {ATTR_SUPPORTED_FEATURES: SUPPORT_TURN_ON | SUPPORT_TURN_OFF}, + {CONF_MODE: [ON_OFF]}), ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py new file mode 100644 index 00000000000..03135b1418e --- /dev/null +++ b/tests/components/homekit/test_type_media_players.py @@ -0,0 +1,106 @@ +"""Test different accessory types: Media Players.""" + +from homeassistant.components.media_player import ( + ATTR_MEDIA_VOLUME_MUTED, DOMAIN) +from homeassistant.components.homekit.type_media_players import MediaPlayer +from homeassistant.components.homekit.const import ( + ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_MODE, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_VOLUME_MUTE, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, + STATE_PLAYING) + +from tests.common import async_mock_service + + +async def test_media_player_set_state(hass): + """Test if accessory and HA are updated accordingly.""" + config = {CONF_MODE: [ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE]} + entity_id = 'media_player.test' + + hass.states.async_set(entity_id, None, {ATTR_SUPPORTED_FEATURES: 20873, + ATTR_MEDIA_VOLUME_MUTED: False}) + await hass.async_block_till_done() + acc = MediaPlayer(hass, 'MediaPlayer', entity_id, 2, config) + await hass.async_add_job(acc.run) + + assert acc.aid == 2 + assert acc.category == 8 # Switch + + assert acc.chars[ON_OFF].value == 0 + assert acc.chars[PLAY_PAUSE].value == 0 + assert acc.chars[PLAY_STOP].value == 0 + assert acc.chars[TOGGLE_MUTE].value == 0 + + hass.states.async_set(entity_id, STATE_ON, {ATTR_MEDIA_VOLUME_MUTED: True}) + await hass.async_block_till_done() + assert acc.chars[ON_OFF].value == 1 + assert acc.chars[TOGGLE_MUTE].value == 1 + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.chars[ON_OFF].value == 0 + + hass.states.async_set(entity_id, STATE_PLAYING) + await hass.async_block_till_done() + assert acc.chars[PLAY_PAUSE].value == 1 + assert acc.chars[PLAY_STOP].value == 1 + + hass.states.async_set(entity_id, STATE_PAUSED) + await hass.async_block_till_done() + assert acc.chars[PLAY_PAUSE].value == 0 + + hass.states.async_set(entity_id, STATE_IDLE) + await hass.async_block_till_done() + assert acc.chars[PLAY_STOP].value == 0 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + call_turn_off = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) + call_media_play = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_PLAY) + call_media_pause = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_PAUSE) + call_media_stop = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_STOP) + call_toggle_mute = async_mock_service(hass, DOMAIN, SERVICE_VOLUME_MUTE) + + await hass.async_add_job(acc.chars[ON_OFF].client_update_value, True) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[ON_OFF].client_update_value, False) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[PLAY_PAUSE].client_update_value, True) + await hass.async_block_till_done() + assert call_media_play + assert call_media_play[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[PLAY_PAUSE].client_update_value, False) + await hass.async_block_till_done() + assert call_media_pause + assert call_media_pause[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[PLAY_STOP].client_update_value, True) + await hass.async_block_till_done() + assert call_media_play + assert call_media_play[1].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[PLAY_STOP].client_update_value, False) + await hass.async_block_till_done() + assert call_media_stop + assert call_media_stop[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[TOGGLE_MUTE].client_update_value, True) + await hass.async_block_till_done() + assert call_toggle_mute + assert call_toggle_mute[0].data[ATTR_ENTITY_ID] == entity_id + assert call_toggle_mute[0].data[ATTR_MEDIA_VOLUME_MUTED] is True + + await hass.async_add_job(acc.chars[TOGGLE_MUTE].client_update_value, False) + await hass.async_block_till_done() + assert call_toggle_mute + assert call_toggle_mute[1].data[ATTR_ENTITY_ID] == entity_id + assert call_toggle_mute[1].data[ATTR_MEDIA_VOLUME_MUTED] is False diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 0755e8f54d4..56a625e02d7 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -2,16 +2,20 @@ import pytest import voluptuous as vol -from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID +from homeassistant.core import State +from homeassistant.components.homekit.const import ( + HOMEKIT_NOTIFY_ID, ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) from homeassistant.components.homekit.util import ( convert_to_float, density_to_air_quality, dismiss_setup_message, - show_setup_message, temperature_to_homekit, temperature_to_states) + show_setup_message, temperature_to_homekit, temperature_to_states, + validate_media_player_modes) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN) from homeassistant.const import ( - ATTR_CODE, CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_MODE, CONF_NAME, STATE_UNKNOWN, + TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service @@ -20,7 +24,8 @@ def test_validate_entity_config(): """Test validate entities.""" configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, {'demo.test': 'test'}, {'demo.test': [1, 2]}, - {'demo.test': None}, {'demo.test': {CONF_NAME: None}}] + {'demo.test': None}, {'demo.test': {CONF_NAME: None}}, + {'media_player.test': {CONF_MODE: 'invalid_mode'}}] for conf in configs: with pytest.raises(vol.Invalid): @@ -39,6 +44,25 @@ def test_validate_entity_config(): assert vec({'lock.demo': {ATTR_CODE: '1234'}}) == \ {'lock.demo': {ATTR_CODE: '1234'}} + assert vec({'media_player.demo': {}}) == \ + {'media_player.demo': {CONF_MODE: []}} + assert vec({'media_player.demo': {CONF_MODE: [ON_OFF]}}) == \ + {'media_player.demo': {CONF_MODE: [ON_OFF]}} + + +def test_validate_media_player_modes(): + """Test validate modes for media players.""" + config = {} + attrs = {ATTR_SUPPORTED_FEATURES: 20873} + entity_state = State('media_player.demo', 'on', attrs) + validate_media_player_modes(entity_state, config) + assert config == {CONF_MODE: [ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE]} + + entity_state = State('media_player.demo', 'on') + config = {CONF_MODE: [ON_OFF]} + with pytest.raises(vol.Invalid): + validate_media_player_modes(entity_state, config) + def test_convert_to_float(): """Test convert_to_float method.""" From 143be49c668ac6512237f20520c1adae877f3f53 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Fri, 25 May 2018 05:38:48 -0400 Subject: [PATCH 040/137] Add HomeKit support for automations (#14595) --- homeassistant/components/homekit/__init__.py | 3 ++- tests/components/homekit/test_get_accessories.py | 6 ++++-- tests/components/homekit/test_type_switches.py | 7 ++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 561301cdb6d..f011a56a77b 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -149,7 +149,8 @@ def get_accessory(hass, state, aid, config): elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'): a_type = 'LightSensor' - elif state.domain in ('switch', 'remote', 'input_boolean', 'script'): + elif state.domain in ('automation', 'input_boolean', 'remote', 'script', + 'switch'): a_type = 'Switch' if a_type is None: diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 6f6d39e477a..11b2d737a70 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -129,9 +129,11 @@ def test_type_sensors(type_name, entity_id, state, attrs): @pytest.mark.parametrize('type_name, entity_id, state, attrs', [ - ('Switch', 'switch.test', 'on', {}), - ('Switch', 'remote.test', 'on', {}), + ('Switch', 'automation.test', 'on', {}), ('Switch', 'input_boolean.test', 'on', {}), + ('Switch', 'remote.test', 'on', {}), + ('Switch', 'script.test', 'on', {}), + ('Switch', 'switch.test', 'on', {}), ]) def test_type_switches(type_name, entity_id, state, attrs): """Test if switch types are associated correctly.""" diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 5fc0b6ce1b9..ff94c4b6a0b 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -9,7 +9,12 @@ from tests.common import async_mock_service @pytest.mark.parametrize('entity_id', [ - 'switch.test', 'remote.test', 'input_boolean.test']) + 'automation.test', + 'input_boolean.test', + 'remote.test', + 'script.test', + 'switch.test', +]) async def test_switch_set_state(hass, entity_id): """Test if accessory and HA are updated accordingly.""" domain = split_entity_id(entity_id)[0] From b4f8d52fb10e64408849efff3f9d54d4e00e5b4c Mon Sep 17 00:00:00 2001 From: Marius Kotlarz Date: Fri, 25 May 2018 15:39:04 +0200 Subject: [PATCH 041/137] Add configurable decimal rounding of display value for CoinMarketCap sensor and upgrade to 5.0.3 (#14437) (#14604) --- .../components/sensor/coinmarketcap.py | 82 ++++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensor/test_coinmarketcap.py | 5 +- tests/fixtures/coinmarketcap.json | 55 ++++++++----- 5 files changed, 93 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index 849e21a0901..f4b666f1e5c 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -13,64 +13,78 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_CURRENCY, CONF_DISPLAY_CURRENCY) + ATTR_ATTRIBUTION, CONF_DISPLAY_CURRENCY) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['coinmarketcap==4.2.1'] +REQUIREMENTS = ['coinmarketcap==5.0.3'] _LOGGER = logging.getLogger(__name__) -ATTR_24H_VOLUME = '24h_volume' +ATTR_VOLUME_24H = 'volume_24h' ATTR_AVAILABLE_SUPPLY = 'available_supply' +ATTR_CIRCULATING_SUPPLY = 'circulating_supply' ATTR_MARKET_CAP = 'market_cap' ATTR_PERCENT_CHANGE_24H = 'percent_change_24h' ATTR_PERCENT_CHANGE_7D = 'percent_change_7d' ATTR_PERCENT_CHANGE_1H = 'percent_change_1h' ATTR_PRICE = 'price' +ATTR_RANK = 'rank' ATTR_SYMBOL = 'symbol' ATTR_TOTAL_SUPPLY = 'total_supply' CONF_ATTRIBUTION = "Data provided by CoinMarketCap" +CONF_CURRENCY_ID = 'currency_id' +CONF_DISPLAY_CURRENCY_DECIMALS = 'display_currency_decimals' -DEFAULT_CURRENCY = 'bitcoin' +DEFAULT_CURRENCY_ID = 1 DEFAULT_DISPLAY_CURRENCY = 'USD' +DEFAULT_DISPLAY_CURRENCY_DECIMALS = 2 ICON = 'mdi:currency-usd' SCAN_INTERVAL = timedelta(minutes=15) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_CURRENCY, default=DEFAULT_CURRENCY): cv.string, + vol.Optional(CONF_CURRENCY_ID, default=DEFAULT_CURRENCY_ID): + cv.positive_int, vol.Optional(CONF_DISPLAY_CURRENCY, default=DEFAULT_DISPLAY_CURRENCY): cv.string, + vol.Optional(CONF_DISPLAY_CURRENCY_DECIMALS, + default=DEFAULT_DISPLAY_CURRENCY_DECIMALS): + vol.All(vol.Coerce(int), vol.Range(min=1)), }) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the CoinMarketCap sensor.""" - currency = config.get(CONF_CURRENCY) - display_currency = config.get(CONF_DISPLAY_CURRENCY).lower() + currency_id = config.get(CONF_CURRENCY_ID) + display_currency = config.get(CONF_DISPLAY_CURRENCY).upper() + display_currency_decimals = config.get(CONF_DISPLAY_CURRENCY_DECIMALS) try: - CoinMarketCapData(currency, display_currency).update() + CoinMarketCapData(currency_id, display_currency).update() except HTTPError: - _LOGGER.warning("Currency %s or display currency %s is not available. " - "Using bitcoin and USD.", currency, display_currency) - currency = DEFAULT_CURRENCY + _LOGGER.warning("Currency ID %s or display currency %s " + "is not available. Using 1 (bitcoin) " + "and USD.", currency_id, display_currency) + currency_id = DEFAULT_CURRENCY_ID display_currency = DEFAULT_DISPLAY_CURRENCY add_devices([CoinMarketCapSensor( - CoinMarketCapData(currency, display_currency))], True) + CoinMarketCapData( + currency_id, display_currency), display_currency_decimals)], True) class CoinMarketCapSensor(Entity): """Representation of a CoinMarketCap sensor.""" - def __init__(self, data): + def __init__(self, data, display_currency_decimals): """Initialize the sensor.""" self.data = data + self.display_currency_decimals = display_currency_decimals self._ticker = None - self._unit_of_measurement = self.data.display_currency.upper() + self._unit_of_measurement = self.data.display_currency @property def name(self): @@ -80,8 +94,9 @@ class CoinMarketCapSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return round(float(self._ticker.get( - 'price_{}'.format(self.data.display_currency))), 2) + return round(float( + self._ticker.get('quotes').get(self.data.display_currency) + .get('price')), self.display_currency_decimals) @property def unit_of_measurement(self): @@ -97,15 +112,24 @@ class CoinMarketCapSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_24H_VOLUME: self._ticker.get( - '24h_volume_{}'.format(self.data.display_currency)), + ATTR_VOLUME_24H: + self._ticker.get('quotes').get(self.data.display_currency) + .get('volume_24h'), ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_AVAILABLE_SUPPLY: self._ticker.get('available_supply'), - ATTR_MARKET_CAP: self._ticker.get( - 'market_cap_{}'.format(self.data.display_currency)), - ATTR_PERCENT_CHANGE_24H: self._ticker.get('percent_change_24h'), - ATTR_PERCENT_CHANGE_7D: self._ticker.get('percent_change_7d'), - ATTR_PERCENT_CHANGE_1H: self._ticker.get('percent_change_1h'), + ATTR_CIRCULATING_SUPPLY: self._ticker.get('circulating_supply'), + ATTR_MARKET_CAP: + self._ticker.get('quotes').get(self.data.display_currency) + .get('market_cap'), + ATTR_PERCENT_CHANGE_24H: + self._ticker.get('quotes').get(self.data.display_currency) + .get('percent_change_24h'), + ATTR_PERCENT_CHANGE_7D: + self._ticker.get('quotes').get(self.data.display_currency) + .get('percent_change_7d'), + ATTR_PERCENT_CHANGE_1H: + self._ticker.get('quotes').get(self.data.display_currency) + .get('percent_change_1h'), + ATTR_RANK: self._ticker.get('rank'), ATTR_SYMBOL: self._ticker.get('symbol'), ATTR_TOTAL_SUPPLY: self._ticker.get('total_supply'), } @@ -113,20 +137,20 @@ class CoinMarketCapSensor(Entity): def update(self): """Get the latest data and updates the states.""" self.data.update() - self._ticker = self.data.ticker[0] + self._ticker = self.data.ticker.get('data') class CoinMarketCapData(object): """Get the latest data and update the states.""" - def __init__(self, currency, display_currency): + def __init__(self, currency_id, display_currency): """Initialize the data object.""" - self.currency = currency + self.currency_id = currency_id self.display_currency = display_currency self.ticker = None def update(self): - """Get the latest data from blockchain.info.""" + """Get the latest data from coinmarketcap.com.""" from coinmarketcap import Market self.ticker = Market().ticker( - self.currency, limit=1, convert=self.display_currency) + self.currency_id, convert=self.display_currency) diff --git a/requirements_all.txt b/requirements_all.txt index d50fe829712..e9c0aaffdf9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -199,7 +199,7 @@ ciscosparkapi==0.4.2 coinbase==2.1.0 # homeassistant.components.sensor.coinmarketcap -coinmarketcap==4.2.1 +coinmarketcap==5.0.3 # homeassistant.scripts.check_config colorlog==3.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 275a1974104..9deebf797eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -44,7 +44,7 @@ apns2==0.3.0 caldav==0.5.0 # homeassistant.components.sensor.coinmarketcap -coinmarketcap==4.2.1 +coinmarketcap==5.0.3 # homeassistant.components.device_tracker.upc_connect defusedxml==0.5.0 diff --git a/tests/components/sensor/test_coinmarketcap.py b/tests/components/sensor/test_coinmarketcap.py index 15c254bfb27..37a63e5cba5 100644 --- a/tests/components/sensor/test_coinmarketcap.py +++ b/tests/components/sensor/test_coinmarketcap.py @@ -11,8 +11,9 @@ from tests.common import ( VALID_CONFIG = { 'platform': 'coinmarketcap', - 'currency': 'ethereum', + 'currency_id': 1027, 'display_currency': 'EUR', + 'display_currency_decimals': 3 } @@ -39,6 +40,6 @@ class TestCoinMarketCapSensor(unittest.TestCase): state = self.hass.states.get('sensor.ethereum') assert state is not None - assert state.state == '240.47' + assert state.state == '493.455' assert state.attributes.get('symbol') == 'ETH' assert state.attributes.get('unit_of_measurement') == 'EUR' diff --git a/tests/fixtures/coinmarketcap.json b/tests/fixtures/coinmarketcap.json index 20f5e4fe91e..5a6b63c5da1 100644 --- a/tests/fixtures/coinmarketcap.json +++ b/tests/fixtures/coinmarketcap.json @@ -1,21 +1,36 @@ -[ - { - "id": "ethereum", - "name": "Ethereum", - "symbol": "ETH", - "rank": "2", - "price_usd": "282.423", - "price_btc": "0.048844", - "24h_volume_usd": "407024000.0", - "market_cap_usd": "26908205315.0", - "available_supply": "95276253.0", - "total_supply": "95276253.0", - "percent_change_1h": "0.06", - "percent_change_24h": "-4.57", - "percent_change_7d": "-16.39", - "last_updated": "1508776751", - "price_eur": "240.473299695", - "24h_volume_eur": "346566690.16", - "market_cap_eur": "22911395039.0" +{ + "cached": false, + "data": { + "id": 1027, + "name": "Ethereum", + "symbol": "ETH", + "website_slug": "ethereum", + "rank": 2, + "circulating_supply": 99619842.0, + "total_supply": 99619842.0, + "max_supply": null, + "quotes": { + "USD": { + "price": 577.019, + "volume_24h": 2839960000.0, + "market_cap": 57482541899.0, + "percent_change_1h": -2.28, + "percent_change_24h": -14.88, + "percent_change_7d": -17.51 + }, + "EUR": { + "price": 493.454724572, + "volume_24h": 2428699712.48, + "market_cap": 49158380042.0, + "percent_change_1h": -2.28, + "percent_change_24h": -14.88, + "percent_change_7d": -17.51 + } + }, + "last_updated": 1527098658 + }, + "metadata": { + "timestamp": 1527098716, + "error": null } -] \ No newline at end of file +} \ No newline at end of file From bf3ead33596e260ec6bc3630df3d1559fa86d840 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 May 2018 11:32:45 -0400 Subject: [PATCH 042/137] Use libsodium18 (#14624) --- virtualization/Docker/setup_docker_prereqs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 3bb4136c991..97c3c6bdd1c 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -23,7 +23,7 @@ PACKAGES=( # homeassistant.components.device_tracker.bluetooth_tracker bluetooth libglib2.0-dev libbluetooth-dev # homeassistant.components.device_tracker.owntracks - libsodium13 + libsodium18 # homeassistant.components.zwave libudev-dev # homeassistant.components.homekit_controller From 48972c75708b2a55194d0e9e25750c99c0c9529a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 May 2018 13:49:45 -0400 Subject: [PATCH 043/137] No longer use backports for ffmpeg (#14626) --- virtualization/Docker/scripts/ffmpeg | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/virtualization/Docker/scripts/ffmpeg b/virtualization/Docker/scripts/ffmpeg index 81b9ce694f9..914c2648e56 100755 --- a/virtualization/Docker/scripts/ffmpeg +++ b/virtualization/Docker/scripts/ffmpeg @@ -8,9 +8,4 @@ PACKAGES=( ffmpeg ) -# Add jessie-backports -echo "Adding jessie-backports" -echo "deb http://deb.debian.org/debian jessie-backports main" >> /etc/apt/sources.list -apt-get update - -apt-get install -y --no-install-recommends -t jessie-backports ${PACKAGES[@]} \ No newline at end of file +apt-get install -y --no-install-recommends ${PACKAGES[@]} From 6ceafabd786c8c688e9fb380ffbbda110c02fa30 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 25 May 2018 22:41:50 +0200 Subject: [PATCH 044/137] Extend package support (#14611) --- homeassistant/config.py | 32 ++++++++++++++--- tests/test_config.py | 80 +++++++++++++++++++++++++++++++++++------ 2 files changed, 98 insertions(+), 14 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 2f916e69b76..44bf542f7cd 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -548,6 +548,31 @@ def _identify_config_schema(module): return '', schema +def _recursive_merge(pack_name, comp_name, config, conf, package): + """Merge package into conf, recursively.""" + for key, pack_conf in package.items(): + if isinstance(pack_conf, dict): + if not pack_conf: + continue + conf[key] = conf.get(key, OrderedDict()) + _recursive_merge(pack_name, comp_name, config, + conf=conf[key], package=pack_conf) + + elif isinstance(pack_conf, list): + if not pack_conf: + continue + conf[key] = cv.ensure_list(conf.get(key)) + conf[key].extend(cv.ensure_list(pack_conf)) + + else: + if conf.get(key) is not None: + _log_pkg_error( + pack_name, comp_name, config, + 'has keys that are defined multiple times') + else: + conf[key] = pack_conf + + def merge_packages_config(hass, config, packages, _log_pkg_error=_log_pkg_error): """Merge packages into the top-level configuration. Mutate config.""" @@ -607,11 +632,10 @@ def merge_packages_config(hass, config, packages, config[comp_name][key] = val continue - # The last merge type are sections that may occur only once + # The last merge type are sections that require recursive merging if comp_name in config: - _log_pkg_error( - pack_name, comp_name, config, "may occur only once" - " and it already exist in your main configuration") + _recursive_merge(pack_name, comp_name, config, + conf=config[comp_name], package=comp_conf) continue config[comp_name] = comp_conf diff --git a/tests/test_config.py b/tests/test_config.py index 4b1115c3814..d22d6b2acfd 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -654,21 +654,81 @@ def test_merge_type_mismatch(merge_log_err, hass): assert len(config['light']) == 2 -def test_merge_once_only(merge_log_err, hass): - """Test if we have a merge for a comp that may occur only once.""" - packages = { - 'pack_2': { - 'mqtt': {}, - 'api': {}, # No config schema - }, - } +def test_merge_once_only_keys(merge_log_err, hass): + """Test if we have a merge for a comp that may occur only once. Keys.""" + packages = {'pack_2': {'api': { + 'key_3': 3, + }}} config = { config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, - 'mqtt': {}, 'api': {} + 'api': { + 'key_1': 1, + 'key_2': 2, + } + } + config_util.merge_packages_config(hass, config, packages) + assert config['api'] == {'key_1': 1, 'key_2': 2, 'key_3': 3, } + + # Duplicate keys error + packages = {'pack_2': {'api': { + 'key': 2, + }}} + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'api': {'key': 1, } } config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 1 - assert len(config) == 3 + + +def test_merge_once_only_lists(hass): + """Test if we have a merge for a comp that may occur only once. Lists.""" + packages = {'pack_2': {'api': { + 'list_1': ['item_2', 'item_3'], + 'list_2': ['item_1'], + 'list_3': [], + }}} + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'api': { + 'list_1': ['item_1'], + } + } + config_util.merge_packages_config(hass, config, packages) + assert config['api'] == { + 'list_1': ['item_1', 'item_2', 'item_3'], + 'list_2': ['item_1'], + } + + +def test_merge_once_only_dictionaries(hass): + """Test if we have a merge for a comp that may occur only once. Dicts.""" + packages = {'pack_2': {'api': { + 'dict_1': { + 'key_2': 2, + 'dict_1.1': {'key_1.2': 1.2, }, + }, + 'dict_2': {'key_1': 1, }, + 'dict_3': {}, + }}} + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'api': { + 'dict_1': { + 'key_1': 1, + 'dict_1.1': {'key_1.1': 1.1, } + }, + } + } + config_util.merge_packages_config(hass, config, packages) + assert config['api'] == { + 'dict_1': { + 'key_1': 1, + 'key_2': 2, + 'dict_1.1': {'key_1.1': 1.1, 'key_1.2': 1.2, }, + }, + 'dict_2': {'key_1': 1, }, + } def test_merge_id_schema(hass): From edfc54b2ebfc4994007245ca551c14d0d121ea4d Mon Sep 17 00:00:00 2001 From: Lorenz Schmid Date: Sat, 26 May 2018 09:51:21 +0200 Subject: [PATCH 045/137] Added option to connect via SSL for OpenWRT(luci) device tracker (#14627) * Added option to connect via HTTPS for OpenWRT(luci) device tracker * Use string formatting * Update quotes * Remove whitespace --- .../components/device_tracker/luci.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index a4b826a009f..f479dea184b 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -15,14 +15,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import HomeAssistantError from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_SSL) _LOGGER = logging.getLogger(__name__) +DEFAULT_SSL = False + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean }) @@ -44,7 +48,9 @@ class LuciDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - self.host = config[CONF_HOST] + host = config[CONF_HOST] + protocol = 'http' if not config[CONF_SSL] else 'https' + self.origin = '{}://{}'.format(protocol, host) self.username = config[CONF_USERNAME] self.password = config[CONF_PASSWORD] @@ -57,7 +63,7 @@ class LuciDeviceScanner(DeviceScanner): def refresh_token(self): """Get a new token.""" - self.token = _get_token(self.host, self.username, self.password) + self.token = _get_token(self.origin, self.username, self.password) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -67,9 +73,9 @@ class LuciDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" if self.mac2name is None: - url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host) - result = _req_json_rpc(url, 'get_all', 'dhcp', - params={'auth': self.token}) + url = '{}/cgi-bin/luci/rpc/uci'.format(self.origin) + result = _req_json_rpc( + url, 'get_all', 'dhcp', params={'auth': self.token}) if result: hosts = [x for x in result.values() if x['.type'] == 'host' and @@ -92,11 +98,11 @@ class LuciDeviceScanner(DeviceScanner): _LOGGER.info("Checking ARP") - url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host) + url = '{}/cgi-bin/luci/rpc/sys'.format(self.origin) try: - result = _req_json_rpc(url, 'net.arptable', - params={'auth': self.token}) + result = _req_json_rpc( + url, 'net.arptable', params={'auth': self.token}) except InvalidLuciTokenError: _LOGGER.info("Refreshing token") self.refresh_token() @@ -146,10 +152,10 @@ def _req_json_rpc(url, method, *args, **kwargs): raise InvalidLuciTokenError else: - _LOGGER.error('Invalid response from luci: %s', res) + _LOGGER.error("Invalid response from luci: %s", res) -def _get_token(host, username, password): - """Get authentication token for the given host+username+password.""" - url = 'http://{}/cgi-bin/luci/rpc/auth'.format(host) +def _get_token(origin, username, password): + """Get authentication token for the given configuration.""" + url = '{}/cgi-bin/luci/rpc/auth'.format(origin) return _req_json_rpc(url, 'login', username, password) From 28d6910e56a5f7c186928cc7f0f4e650e6308802 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sat, 26 May 2018 07:43:31 -0400 Subject: [PATCH 046/137] Added UDP and parallel streams support to Iperf3 (#14629) --- homeassistant/components/sensor/iperf3.py | 29 ++++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/iperf3.py b/homeassistant/components/sensor/iperf3.py index 1a209faf17f..8e030390f50 100644 --- a/homeassistant/components/sensor/iperf3.py +++ b/homeassistant/components/sensor/iperf3.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ENTITY_ID, CONF_MONITORED_CONDITIONS, - CONF_HOST, CONF_PORT) + CONF_HOST, CONF_PORT, CONF_PROTOCOL) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -27,13 +27,16 @@ ATTR_VERSION = 'Version' CONF_ATTRIBUTION = 'Data retrieved using Iperf3' CONF_DURATION = 'duration' +CONF_PARALLEL = 'parallel' DEFAULT_DURATION = 10 DEFAULT_PORT = 5201 +DEFAULT_PARALLEL = 1 +DEFAULT_PROTOCOL = 'tcp' IPERF3_DATA = 'iperf3' -SCAN_INTERVAL = timedelta(minutes=30) +SCAN_INTERVAL = timedelta(minutes=60) SERVICE_NAME = 'iperf3_update' @@ -50,6 +53,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Range(5, 10), + vol.Optional(CONF_PARALLEL, default=DEFAULT_PARALLEL): vol.Range(1, 20), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): + vol.In(['tcp', 'udp']), }) @@ -70,6 +76,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): Iperf3Sensor(config[CONF_HOST], config[CONF_PORT], config[CONF_DURATION], + config[CONF_PARALLEL], + config[CONF_PROTOCOL], sensor)) hass.data[IPERF3_DATA]['sensors'].extend(dev) @@ -98,10 +106,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class Iperf3Sensor(Entity): """A Iperf3 sensor implementation.""" - def __init__(self, server, port, duration, sensor_type): + def __init__(self, server, port, duration, streams, + protocol, sensor_type): """Initialize the sensor.""" self._attrs = { ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_PROTOCOL: protocol, } self._name = \ "{} {}".format(SENSOR_TYPES[sensor_type][0], server) @@ -111,6 +121,8 @@ class Iperf3Sensor(Entity): self._port = port self._server = server self._duration = duration + self._num_streams = streams + self._protocol = protocol self.result = None @property @@ -133,7 +145,6 @@ class Iperf3Sensor(Entity): """Return the state attributes.""" if self.result is not None: self._attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION - self._attrs[ATTR_PROTOCOL] = self.result.protocol self._attrs[ATTR_REMOTE_HOST] = self.result.remote_host self._attrs[ATTR_REMOTE_PORT] = self.result.remote_port self._attrs[ATTR_VERSION] = self.result.version @@ -147,6 +158,8 @@ class Iperf3Sensor(Entity): client.server_hostname = self._server client.port = self._port client.verbose = False + client.num_streams = self._num_streams + client.protocol = self._protocol # when testing download bandwith, reverse must be True if self._sensor_type == 'download': @@ -154,7 +167,7 @@ class Iperf3Sensor(Entity): try: self.result = client.run() - except (OSError, AttributeError) as error: + except (AttributeError, OSError, ValueError) as error: self.result = None _LOGGER.error("Iperf3 sensor error: %s", error) return @@ -166,7 +179,11 @@ class Iperf3Sensor(Entity): self.result = None return - if self._sensor_type == 'download': + # UDP only have 1 way attribute + if self._protocol == 'udp': + self._state = round(self.result.Mbps, 2) + + elif self._sensor_type == 'download': self._state = round(self.result.received_Mbps, 2) elif self._sensor_type == 'upload': From a55fbd2be79fdf08c690252a94512708ae8ee857 Mon Sep 17 00:00:00 2001 From: Max Muth Date: Sat, 26 May 2018 13:53:48 +0200 Subject: [PATCH 047/137] Add services for adding and removing items to shopping list (#14574) --- homeassistant/components/services.yaml | 14 ++++++++ homeassistant/components/shopping_list.py | 41 ++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 746c3c7f483..c0279ef1d0f 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -556,3 +556,17 @@ xiaomi_aqara: device_id: description: Hardware address of the device to remove. example: 158d0000000000 + +shopping_list: + add_item: + description: Adds an item to the shopping list. + fields: + name: + description: The name of the item to add. + example: Beer + complete_item: + description: Marks an item as completed in the shopping list. It does not remove the item. + fields: + name: + description: The name of the item to mark as completed. + example: Beer diff --git a/homeassistant/components/shopping_list.py b/homeassistant/components/shopping_list.py index 0ca0fef6e06..f113561429a 100644 --- a/homeassistant/components/shopping_list.py +++ b/homeassistant/components/shopping_list.py @@ -14,6 +14,8 @@ from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json +ATTR_NAME = 'name' + DOMAIN = 'shopping_list' DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) @@ -23,20 +25,57 @@ INTENT_ADD_ITEM = 'HassShoppingListAddItem' INTENT_LAST_ITEMS = 'HassShoppingListLastItems' ITEM_UPDATE_SCHEMA = vol.Schema({ 'complete': bool, - 'name': str, + ATTR_NAME: str, }) PERSISTENCE = '.shopping_list.json' +SERVICE_ADD_ITEM = 'add_item' +SERVICE_COMPLETE_ITEM = 'complete_item' + +SERVICE_ITEM_SCHEMA = vol.Schema({ + vol.Required(ATTR_NAME): vol.Any(None, cv.string) +}) + @asyncio.coroutine def async_setup(hass, config): """Initialize the shopping list.""" + @asyncio.coroutine + def add_item_service(call): + """Add an item with `name`.""" + data = hass.data[DOMAIN] + name = call.data.get(ATTR_NAME) + if name is not None: + data.async_add(name) + + @asyncio.coroutine + def complete_item_service(call): + """Mark the item provided via `name` as completed.""" + data = hass.data[DOMAIN] + name = call.data.get(ATTR_NAME) + if name is None: + return + try: + item = [item for item in data.items if item['name'] == name][0] + except IndexError: + _LOGGER.error("Removing of item failed: %s cannot be found", name) + else: + data.async_update(item['id'], {'name': name, 'complete': True}) + data = hass.data[DOMAIN] = ShoppingData(hass) yield from data.async_load() intent.async_register(hass, AddItemIntent()) intent.async_register(hass, ListTopItemsIntent()) + hass.services.async_register( + DOMAIN, SERVICE_ADD_ITEM, add_item_service, schema=SERVICE_ITEM_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_COMPLETE_ITEM, complete_item_service, + schema=SERVICE_ITEM_SCHEMA + ) + hass.http.register_view(ShoppingListView) hass.http.register_view(CreateShoppingListItemView) hass.http.register_view(UpdateShoppingListItemView) From 41fc44b27c2c35244ed5ebbf5ae61ca9d97908f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 08:33:22 -0400 Subject: [PATCH 048/137] Bump frontend to 20180526.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8ee6ce549a4..f6b8bc9cb7a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180524.0'] +REQUIREMENTS = ['home-assistant-frontend==20180526.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index e9c0aaffdf9..08296b91c7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180524.0 +home-assistant-frontend==20180526.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9deebf797eb..12d2d2154e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180524.0 +home-assistant-frontend==20180526.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 7ea25cd3600603ac198807c0ef7bf7a689b7154e Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Sat, 26 May 2018 16:03:53 +0200 Subject: [PATCH 049/137] Add homematicip cloud climate platform (#14388) * Add support for climatic devices * Update requirements_all * Change icon to mdi:thermostat * Update of homematicip-rest-api lib version * Remove all mode or state relevant things - will come later * Add current_operation again to see proper status * Remove STATE_PERFORMANCE import * Remove trailing whitespace * Update requirements file --- .../components/climate/homematicip_cloud.py | 101 ++++++++++++++++++ homeassistant/components/homematicip_cloud.py | 5 +- requirements_all.txt | 2 +- 3 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/climate/homematicip_cloud.py diff --git a/homeassistant/components/climate/homematicip_cloud.py b/homeassistant/components/climate/homematicip_cloud.py new file mode 100644 index 00000000000..bf96f1f746d --- /dev/null +++ b/homeassistant/components/climate/homematicip_cloud.py @@ -0,0 +1,101 @@ +""" +Support for HomematicIP climate. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/climate.homematicip_cloud/ +""" + +import logging + +from homeassistant.components.climate import ( + ClimateDevice, SUPPORT_TARGET_TEMPERATURE, ATTR_TEMPERATURE, + STATE_AUTO, STATE_MANUAL) +from homeassistant.const import TEMP_CELSIUS +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) + +_LOGGER = logging.getLogger(__name__) + +STATE_BOOST = 'Boost' + +HA_STATE_TO_HMIP = { + STATE_AUTO: 'AUTOMATIC', + STATE_MANUAL: 'MANUAL', +} + +HMIP_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_HMIP.items()} + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the HomematicIP climate devices.""" + from homematicip.group import HeatingGroup + + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + + devices = [] + for device in home.groups: + if isinstance(device, HeatingGroup): + devices.append(HomematicipHeatingGroup(home, device)) + + if devices: + async_add_devices(devices) + + +class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): + """Representation of a MomematicIP heating group.""" + + def __init__(self, home, device): + """Initialize heating group.""" + device.modelType = 'Group-Heating' + super().__init__(home, device) + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_TARGET_TEMPERATURE + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._device.setPointTemperature + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._device.actualTemperature + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._device.humidity + + @property + def current_operation(self): + """Return current operation ie. automatic or manual.""" + return HMIP_STATE_TO_HA.get(self._device.controlMode) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._device.minTemperature + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._device.maxTemperature + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + await self._device.set_point_temperature(temperature) diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index d85d867d8f8..859841dfca6 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -17,7 +17,7 @@ from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity from homeassistant.core import callback -REQUIREMENTS = ['homematicip==0.9.2.4'] +REQUIREMENTS = ['homematicip==0.9.4'] _LOGGER = logging.getLogger(__name__) @@ -27,7 +27,8 @@ COMPONENTS = [ 'sensor', 'binary_sensor', 'switch', - 'light' + 'light', + 'climate', ] CONF_NAME = 'name' diff --git a/requirements_all.txt b/requirements_all.txt index 08296b91c7b..f172dcb13e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -395,7 +395,7 @@ home-assistant-frontend==20180526.1 # homekit==0.6 # homeassistant.components.homematicip_cloud -homematicip==0.9.2.4 +homematicip==0.9.4 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a From 8de56cfc1057de9de54fd3391d5887944605827d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 26 May 2018 17:35:16 +0200 Subject: [PATCH 050/137] Upgrade speedtest-cli to 2.0.2 (#14633) --- homeassistant/components/sensor/speedtest.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index 5b03be036d5..bf2868d3b01 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -17,7 +17,7 @@ from homeassistant.helpers.event import track_time_change from homeassistant.helpers.restore_state import async_get_last_state import homeassistant.util.dt as dt_util -REQUIREMENTS = ['speedtest-cli==2.0.0'] +REQUIREMENTS = ['speedtest-cli==2.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f172dcb13e7..3eb75d92cf0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1228,7 +1228,7 @@ socialbladeclient==0.2 somecomfort==0.5.2 # homeassistant.components.sensor.speedtest -speedtest-cli==2.0.0 +speedtest-cli==2.0.2 # homeassistant.components.sensor.spotcrime spotcrime==1.0.3 From fdb250d86c50f486dccbdaa191c7934da791324f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 11:53:24 -0400 Subject: [PATCH 051/137] Bump frontend to 20180526.2 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index f6b8bc9cb7a..7d888a2b082 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.1'] +REQUIREMENTS = ['home-assistant-frontend==20180526.2'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 3eb75d92cf0..b751f592093 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.1 +home-assistant-frontend==20180526.2 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12d2d2154e5..6d1a58bc5d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.1 +home-assistant-frontend==20180526.2 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From dfd7ef1fcecbe4fe13b3ec17d845f21d9f8de179 Mon Sep 17 00:00:00 2001 From: David Ryan Date: Sat, 26 May 2018 12:42:52 -0400 Subject: [PATCH 052/137] Add Hydrawise component (#14055) * Added the Hydrawise component. * Fixed lint errors. * Multiple changes due to review comments addressed. * Simplified boolean test. Passes pylint. * Need hydrawiser package version 0.1.1. * Added a docstring to the device_class method. * Addressed all review comments from MartinHjelmare. * Changed keys to single quote. Removed unnecessary duplicate method. * Removed unused imports. * Changed state to lowercase snakecase. * Changes & fixes from review comments. --- .coveragerc | 3 + .../components/binary_sensor/hydrawise.py | 81 ++++++++++ homeassistant/components/hydrawise.py | 153 ++++++++++++++++++ homeassistant/components/sensor/hydrawise.py | 72 +++++++++ homeassistant/components/switch/hydrawise.py | 103 ++++++++++++ requirements_all.txt | 3 + 6 files changed, 415 insertions(+) create mode 100644 homeassistant/components/binary_sensor/hydrawise.py create mode 100644 homeassistant/components/hydrawise.py create mode 100644 homeassistant/components/sensor/hydrawise.py create mode 100644 homeassistant/components/switch/hydrawise.py diff --git a/.coveragerc b/.coveragerc index 3ccfdeb3569..d4dc4e4367d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -123,6 +123,9 @@ omit = homeassistant/components/homematicip_cloud.py homeassistant/components/*/homematicip_cloud.py + homeassistant/components/hydrawise.py + homeassistant/components/*/hydrawise.py + homeassistant/components/ihc/* homeassistant/components/*/ihc.py diff --git a/homeassistant/components/binary_sensor/hydrawise.py b/homeassistant/components/binary_sensor/hydrawise.py new file mode 100644 index 00000000000..a3e0ebd782d --- /dev/null +++ b/homeassistant/components/binary_sensor/hydrawise.py @@ -0,0 +1,81 @@ +""" +Support for Hydrawise sprinkler. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.hydrawise/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.hydrawise import ( + BINARY_SENSORS, DATA_HYDRAWISE, HydrawiseEntity, DEVICE_MAP, + DEVICE_MAP_INDEX) +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['hydrawise'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=BINARY_SENSORS): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for a Hydrawise device.""" + hydrawise = hass.data[DATA_HYDRAWISE].data + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + if sensor_type in ['status', 'rain_sensor']: + sensors.append( + HydrawiseBinarySensor( + hydrawise.controller_status, sensor_type)) + + else: + # create a sensor for each zone + for zone in hydrawise.relays: + zone_data = zone + zone_data['running'] = \ + hydrawise.controller_status.get('running', False) + sensors.append(HydrawiseBinarySensor(zone_data, sensor_type)) + + add_devices(sensors, True) + + +class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorDevice): + """A sensor implementation for Hydrawise device.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + def update(self): + """Get the latest data and updates the state.""" + _LOGGER.debug("Updating Hydrawise binary sensor: %s", self._name) + mydata = self.hass.data[DATA_HYDRAWISE].data + if self._sensor_type == 'status': + self._state = mydata.status == 'All good!' + elif self._sensor_type == 'rain_sensor': + for sensor in mydata.sensors: + if sensor['name'] == 'Rain': + self._state = sensor['active'] == 1 + elif self._sensor_type == 'is_watering': + if not mydata.running: + self._state = False + elif int(mydata.running[0]['relay']) == self.data['relay']: + self._state = True + else: + self._state = False + + @property + def device_class(self): + """Return the device class of the sensor type.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('DEVICE_CLASS_INDEX')] diff --git a/homeassistant/components/hydrawise.py b/homeassistant/components/hydrawise.py new file mode 100644 index 00000000000..a60e3d5b8fc --- /dev/null +++ b/homeassistant/components/hydrawise.py @@ -0,0 +1,153 @@ +""" +Support for Hydrawise cloud. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/hydrawise/ +""" +import asyncio +from datetime import timedelta +import logging + +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL) +import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +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 = ['hydrawiser==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60] + +CONF_ATTRIBUTION = "Data provided by hydrawise.com" +CONF_WATERING_TIME = 'watering_minutes' + +NOTIFICATION_ID = 'hydrawise_notification' +NOTIFICATION_TITLE = 'Hydrawise Setup' + +DATA_HYDRAWISE = 'hydrawise' +DOMAIN = 'hydrawise' +DEFAULT_WATERING_TIME = 15 + +DEVICE_MAP_INDEX = ['KEY_INDEX', 'ICON_INDEX', 'DEVICE_CLASS_INDEX', + 'UNIT_OF_MEASURE_INDEX'] +DEVICE_MAP = { + 'auto_watering': ['Automatic Watering', 'mdi:autorenew', '', ''], + 'is_watering': ['Watering', '', 'moisture', ''], + 'manual_watering': ['Manual Watering', 'mdi:water-pump', '', ''], + 'next_cycle': ['Next Cycle', 'mdi:calendar-clock', '', ''], + 'status': ['Status', '', 'connectivity', ''], + 'watering_time': ['Watering Time', 'mdi:water-pump', '', 'min'], + 'rain_sensor': ['Rain Sensor', '', 'moisture', ''] +} + +BINARY_SENSORS = ['is_watering', 'status', 'rain_sensor'] + +SENSORS = ['next_cycle', 'watering_time'] + +SWITCHES = ['auto_watering', 'manual_watering'] + +SCAN_INTERVAL = timedelta(seconds=30) + +SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Hunter Hydrawise component.""" + conf = config[DOMAIN] + access_token = conf[CONF_ACCESS_TOKEN] + scan_interval = conf.get(CONF_SCAN_INTERVAL) + + try: + from hydrawiser.core import Hydrawiser + + hydrawise = Hydrawiser(user_token=access_token) + hass.data[DATA_HYDRAWISE] = HydrawiseHub(hydrawise) + except (ConnectTimeout, HTTPError) as ex: + _LOGGER.error( + "Unable to connect to Hydrawise cloud service: %s", str(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 False + + def hub_refresh(event_time): + """Call Hydrawise hub to refresh information.""" + _LOGGER.debug("Updating Hydrawise Hub component") + hass.data[DATA_HYDRAWISE].data.update_controller_info() + dispatcher_send(hass, SIGNAL_UPDATE_HYDRAWISE) + + # Call the Hydrawise API to refresh updates + track_time_interval(hass, hub_refresh, scan_interval) + + return True + + +class HydrawiseHub(object): + """Representation of a base Hydrawise device.""" + + def __init__(self, data): + """Initialize the entity.""" + self.data = data + + +class HydrawiseEntity(Entity): + """Entity class for Hydrawise devices.""" + + def __init__(self, data, sensor_type): + """Initialize the Hydrawise entity.""" + self.data = data + self._sensor_type = sensor_type + self._name = "{0} {1}".format( + self.data['name'], + DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('KEY_INDEX')]) + self._state = None + + @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_HYDRAWISE, self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('UNIT_OF_MEASURE_INDEX')] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + 'identifier': self.data.get('relay'), + } diff --git a/homeassistant/components/sensor/hydrawise.py b/homeassistant/components/sensor/hydrawise.py new file mode 100644 index 00000000000..fea2780da07 --- /dev/null +++ b/homeassistant/components/sensor/hydrawise.py @@ -0,0 +1,72 @@ +""" +Support for Hydrawise sprinkler. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.hydrawise/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.hydrawise import ( + DATA_HYDRAWISE, HydrawiseEntity, DEVICE_MAP, DEVICE_MAP_INDEX, SENSORS) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['hydrawise'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSORS)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for a Hydrawise device.""" + hydrawise = hass.data[DATA_HYDRAWISE].data + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + for zone in hydrawise.relays: + sensors.append(HydrawiseSensor(zone, sensor_type)) + + add_devices(sensors, True) + + +class HydrawiseSensor(HydrawiseEntity): + """A sensor implementation for Hydrawise device.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data and updates the states.""" + mydata = self.hass.data[DATA_HYDRAWISE].data + _LOGGER.debug("Updating Hydrawise sensor: %s", self._name) + if self._sensor_type == 'watering_time': + if not mydata.running: + self._state = 0 + else: + if int(mydata.running[0]['relay']) == self.data['relay']: + self._state = int(mydata.running[0]['time_left']/60) + else: + self._state = 0 + else: # _sensor_type == 'next_cycle' + for relay in mydata.relays: + if relay['relay'] == self.data['relay']: + if relay['nicetime'] == 'Not scheduled': + self._state = 'not_scheduled' + else: + self._state = relay['nicetime'].split(',')[0] + \ + ' ' + relay['nicetime'].split(' ')[3] + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('ICON_INDEX')] diff --git a/homeassistant/components/switch/hydrawise.py b/homeassistant/components/switch/hydrawise.py new file mode 100644 index 00000000000..d0abe5febf5 --- /dev/null +++ b/homeassistant/components/switch/hydrawise.py @@ -0,0 +1,103 @@ +""" +Support for Hydrawise cloud. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.hydrawise/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.hydrawise import ( + ALLOWED_WATERING_TIME, CONF_WATERING_TIME, + DATA_HYDRAWISE, DEFAULT_WATERING_TIME, HydrawiseEntity, SWITCHES, + DEVICE_MAP, DEVICE_MAP_INDEX) +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['hydrawise'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=SWITCHES): + vol.All(cv.ensure_list, [vol.In(SWITCHES)]), + vol.Optional(CONF_WATERING_TIME, default=DEFAULT_WATERING_TIME): + vol.All(vol.In(ALLOWED_WATERING_TIME)), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for a Hydrawise device.""" + hydrawise = hass.data[DATA_HYDRAWISE].data + + default_watering_timer = config.get(CONF_WATERING_TIME) + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + # create a switch for each zone + for zone in hydrawise.relays: + sensors.append( + HydrawiseSwitch(default_watering_timer, + zone, + sensor_type)) + + add_devices(sensors, True) + + +class HydrawiseSwitch(HydrawiseEntity, SwitchDevice): + """A switch implementation for Hydrawise device.""" + + def __init__(self, default_watering_timer, *args): + """Initialize a switch for Hydrawise device.""" + super().__init__(*args) + self._default_watering_timer = default_watering_timer + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the device on.""" + if self._sensor_type == 'manual_watering': + self.hass.data[DATA_HYDRAWISE].data.run_zone( + self._default_watering_timer, (self.data['relay']-1)) + elif self._sensor_type == 'auto_watering': + self.hass.data[DATA_HYDRAWISE].data.suspend_zone( + 0, (self.data['relay']-1)) + + def turn_off(self, **kwargs): + """Turn the device off.""" + if self._sensor_type == 'manual_watering': + self.hass.data[DATA_HYDRAWISE].data.run_zone( + 0, (self.data['relay']-1)) + elif self._sensor_type == 'auto_watering': + self.hass.data[DATA_HYDRAWISE].data.suspend_zone( + 365, (self.data['relay']-1)) + + def update(self): + """Update device state.""" + mydata = self.hass.data[DATA_HYDRAWISE].data + _LOGGER.debug("Updating Hydrawise switch: %s", self._name) + if self._sensor_type == 'manual_watering': + if not mydata.running: + self._state = False + else: + self._state = int( + mydata.running[0]['relay']) == self.data['relay'] + elif self._sensor_type == 'auto_watering': + for relay in mydata.relays: + if relay['relay'] == self.data['relay']: + if relay.get('suspended') is not None: + self._state = False + else: + self._state = True + break + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('ICON_INDEX')] diff --git a/requirements_all.txt b/requirements_all.txt index b751f592093..a5f0bcaf78f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,6 +430,9 @@ https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4 # homeassistant.components.media_player.lg_netcast https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 +# homeassistant.components.hydrawise +hydrawiser==0.1.1 + # homeassistant.components.sensor.bh1750 # homeassistant.components.sensor.bme280 # homeassistant.components.sensor.htu21d From bcde57bff89835bfe03acf61f62f8f1decdc6c2c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 14:29:26 -0400 Subject: [PATCH 053/137] Bump frontend to 20180526.3 --- 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 7d888a2b082..654afd67f42 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.2'] +REQUIREMENTS = ['home-assistant-frontend==20180526.3'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index a5f0bcaf78f..934feb4ea42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.2 +home-assistant-frontend==20180526.3 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d1a58bc5d3..7ae86cf65f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.2 +home-assistant-frontend==20180526.3 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From c425afe50eca32a536168da4a952d7b8bbced950 Mon Sep 17 00:00:00 2001 From: guillaume1410 Date: Sat, 26 May 2018 16:46:53 -0400 Subject: [PATCH 054/137] Adding ryobi garage door opener (#14618) * Initial component for Ryobi cover * Initial component for Ryobi cover * Adding Ryobi cover * Adding Ryobi cover * Adding Ryobi cover * Minor changes * Remove import --- .coveragerc | 1 + homeassistant/components/cover/ryobi_gdo.py | 103 ++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 107 insertions(+) create mode 100644 homeassistant/components/cover/ryobi_gdo.py diff --git a/.coveragerc b/.coveragerc index d4dc4e4367d..3d1bbab8456 100644 --- a/.coveragerc +++ b/.coveragerc @@ -385,6 +385,7 @@ omit = homeassistant/components/cover/myq.py homeassistant/components/cover/opengarage.py homeassistant/components/cover/rpi_gpio.py + homeassistant/components/cover/ryobi_gdo.py homeassistant/components/cover/scsgate.py homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py diff --git a/homeassistant/components/cover/ryobi_gdo.py b/homeassistant/components/cover/ryobi_gdo.py new file mode 100644 index 00000000000..a11d70dd3ad --- /dev/null +++ b/homeassistant/components/cover/ryobi_gdo.py @@ -0,0 +1,103 @@ +""" +Ryobi platform for the cover component. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/cover.ryobi_gdo/ +""" +import logging +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +from homeassistant.components.cover import ( + CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE) +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, STATE_UNKNOWN, STATE_CLOSED) + +REQUIREMENTS = ['py_ryobi_gdo==0.0.10'] + +_LOGGER = logging.getLogger(__name__) + +CONF_DEVICE_ID = 'device_id' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, +}) + +SUPPORTED_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Ryobi covers.""" + from py_ryobi_gdo import RyobiGDO as ryobi_door + covers = [] + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + devices = config.get(CONF_DEVICE_ID) + + for device_id in devices: + my_door = ryobi_door(username, password, device_id) + _LOGGER.debug("Getting the API key") + if my_door.get_api_key() is False: + _LOGGER.error("Wrong credentials, no API key retrieved") + return + _LOGGER.debug("Checking if the device ID is present") + if my_door.check_device_id() is False: + _LOGGER.error("%s not in your device list", device_id) + return + _LOGGER.debug("Adding device %s to covers", device_id) + covers.append(RyobiCover(hass, my_door)) + if covers: + _LOGGER.debug("Adding covers") + add_devices(covers, True) + + +class RyobiCover(CoverDevice): + """Representation of a ryobi cover.""" + + def __init__(self, hass, ryobi_door): + """Initialize the cover.""" + self.ryobi_door = ryobi_door + self._name = 'ryobi_gdo_{}'.format(ryobi_door.get_device_id()) + self._door_state = None + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def is_closed(self): + """Return if the cover is closed.""" + if self._door_state == STATE_UNKNOWN: + return False + return self._door_state == 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 SUPPORTED_FEATURES + + def close_cover(self, **kwargs): + """Close the cover.""" + _LOGGER.debug("Closing garage door") + self.ryobi_door.close_device() + + def open_cover(self, **kwargs): + """Open the cover.""" + _LOGGER.debug("Opening garage door") + self.ryobi_door.open_device() + + def update(self): + """Update status from the door.""" + _LOGGER.debug("Updating RyobiGDO status") + self.ryobi_door.update() + self._door_state = self.ryobi_door.get_door_status() diff --git a/requirements_all.txt b/requirements_all.txt index 934feb4ea42..2c3a9b4bf5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -706,6 +706,9 @@ pyTibber==0.4.1 # homeassistant.components.switch.dlink pyW215==0.6.0 +# homeassistant.components.cover.ryobi_gdo +py_ryobi_gdo==0.0.10 + # homeassistant.components.ads pyads==2.2.6 From eae9726beca0b0da84fe06ab94785d4c6a3f4c14 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 26 May 2018 16:50:05 -0400 Subject: [PATCH 055/137] Add electrical measurement sensor to ZHA (#14561) * Add electrical measurement sensor * correct state update * hound fix * zha: Add metering sensor (#14562) * Add IlluminanceMeasurementSensor to ZHA (#14563) * add IlluminanceMeasurementSensor * address review comment * Fix whitespace error during merge * Add electrical measurement sensor * correct state update * hound / flake8 --- homeassistant/components/sensor/zha.py | 38 ++++++++++++++++++++++++++ homeassistant/components/zha/const.py | 1 + 2 files changed, 39 insertions(+) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 72368bdb3ba..984d6efed66 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -36,6 +36,7 @@ def make_sensor(discovery_info): IlluminanceMeasurement ) from zigpy.zcl.clusters.smartenergy import Metering + from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement in_clusters = discovery_info['in_clusters'] if RelativeHumidity.cluster_id in in_clusters: sensor = RelativeHumiditySensor(**discovery_info) @@ -47,6 +48,9 @@ def make_sensor(discovery_info): sensor = IlluminanceMeasurementSensor(**discovery_info) elif Metering.cluster_id in in_clusters: sensor = MeteringSensor(**discovery_info) + elif ElectricalMeasurement.cluster_id in in_clusters: + sensor = ElectricalMeasurementSensor(**discovery_info) + return sensor else: sensor = Sensor(**discovery_info) @@ -182,3 +186,37 @@ class MeteringSensor(Sensor): return None return round(float(self._state)) + + +class ElectricalMeasurementSensor(Sensor): + """ZHA Electrical Measurement sensor.""" + + value_attribute = 1291 + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return 'W' + + @property + def state(self): + """Return the state of the entity.""" + if self._state is None: + return None + + return round(float(self._state) / 10, 1) + + @property + def should_poll(self) -> bool: + """Poll state from device.""" + return True + + async def async_update(self): + """Retrieve latest state.""" + _LOGGER.debug("%s async_update", self.entity_id) + + result = await zha.safe_read( + self._endpoint.electrical_measurement, + ['active_power'], + allow_cache=False) + self._state = result.get('active_power', self._state) diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 087b19c6693..37c7f5592a0 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -50,6 +50,7 @@ def populate_data(): zcl.clusters.measurement.PressureMeasurement: 'sensor', zcl.clusters.measurement.IlluminanceMeasurement: 'sensor', zcl.clusters.smartenergy.Metering: 'sensor', + zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', }) From a5e66ce6bafc3212f78524481dd6aa39471fee3d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 20:02:16 -0400 Subject: [PATCH 056/137] Bump frontend to 20180526.4 --- 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 654afd67f42..2bd7283e38e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.3'] +REQUIREMENTS = ['home-assistant-frontend==20180526.4'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 2c3a9b4bf5c..d6e4ac86976 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.3 +home-assistant-frontend==20180526.4 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ae86cf65f1..5f2ed329637 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.3 +home-assistant-frontend==20180526.4 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 5f9e4ae136e55a1da8aa24513f084686ccd0984e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 May 2018 09:53:53 +0200 Subject: [PATCH 057/137] Upgrade luftdaten to 0.2.0 (#14620) --- homeassistant/components/sensor/luftdaten.py | 9 ++++----- requirements_all.txt | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/luftdaten.py b/homeassistant/components/sensor/luftdaten.py index c5e0b12b0e0..9952e2a1c24 100644 --- a/homeassistant/components/sensor/luftdaten.py +++ b/homeassistant/components/sensor/luftdaten.py @@ -4,7 +4,6 @@ Support for Luftdaten sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.luftdaten/ """ -import asyncio from datetime import timedelta import logging @@ -19,7 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['luftdaten==0.1.3'] +REQUIREMENTS = ['luftdaten==0.2.0'] _LOGGER = logging.getLogger(__name__) @@ -59,8 +58,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@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 Luftdaten sensor.""" from luftdaten import Luftdaten @@ -71,7 +70,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): session = async_get_clientsession(hass) luftdaten = LuftdatenData(Luftdaten(sensor_id, hass.loop, session)) - yield from luftdaten.async_update() + await luftdaten.async_update() if luftdaten.data is None: _LOGGER.error("Sensor is not available: %s", sensor_id) diff --git a/requirements_all.txt b/requirements_all.txt index d6e4ac86976..fe9b088e1f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -518,7 +518,7 @@ lmnotify==0.0.4 locationsharinglib==2.0.2 # homeassistant.components.sensor.luftdaten -luftdaten==0.1.3 +luftdaten==0.2.0 # homeassistant.components.light.lw12wifi lw12==0.9.2 From 5acfe5da68d354782007072341ad7061bf7a3aa6 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sun, 27 May 2018 02:31:05 -0700 Subject: [PATCH 058/137] Upgrade python-nest to 4.0.0 (#14638) * Upgrade python-nest to 4.0.0 Drop in replace to use nest stream API Didn't change any logic from HA side * Update requirements_all.txt --- homeassistant/components/nest.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 2500755d495..f474bfa7a26 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_MONITORED_CONDITIONS) -REQUIREMENTS = ['python-nest==3.7.0'] +REQUIREMENTS = ['python-nest==4.0.0'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fe9b088e1f9..2264c716c5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1027,7 +1027,7 @@ python-mpd2==1.0.0 python-mystrom==0.4.2 # homeassistant.components.nest -python-nest==3.7.0 +python-nest==4.0.0 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 From 2d88f47795cd43655a67d42f1a3b1cafd0b40d9d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 May 2018 15:45:43 +0200 Subject: [PATCH 059/137] Upgrade gitterpy to 0.1.7 (#14643) --- homeassistant/components/sensor/gitter.py | 17 ++++++++++++----- requirements_all.txt | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/gitter.py b/homeassistant/components/sensor/gitter.py index 58f33635750..907af07a2db 100644 --- a/homeassistant/components/sensor/gitter.py +++ b/homeassistant/components/sensor/gitter.py @@ -8,12 +8,12 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_API_KEY, CONF_ROOM +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_ROOM +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['gitterpy==0.1.6'] +REQUIREMENTS = ['gitterpy==0.1.7'] _LOGGER = logging.getLogger(__name__) @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): username = gitter.auth.get_my_id['name'] except GitterTokenError: _LOGGER.error("Token is not valid") - return False + return add_devices([GitterSensor(gitter, room, name, username)], True) @@ -96,7 +96,14 @@ class GitterSensor(Entity): def update(self): """Get the latest data and updates the state.""" - data = self._data.user.unread_items(self._room) + from gitterpy.errors import GitterRoomError + + try: + data = self._data.user.unread_items(self._room) + except GitterRoomError as error: + _LOGGER.error(error) + return + if 'error' not in data.keys(): self._mention = len(data['mention']) self._state = len(data['chat']) diff --git a/requirements_all.txt b/requirements_all.txt index 2264c716c5b..126d28832b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -344,7 +344,7 @@ gTTS-token==1.1.1 gearbest_parser==1.0.5 # homeassistant.components.sensor.gitter -gitterpy==0.1.6 +gitterpy==0.1.7 # homeassistant.components.notify.gntp gntp==1.0.3 From 36e8157268d32ae65ea0e819cf8ba50510416fae Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 May 2018 15:46:58 +0200 Subject: [PATCH 060/137] Upgrade TwitterAPI to 2.5.4 (#14639) --- homeassistant/components/notify/twitter.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index f81a83325ce..e38e7fcaa0f 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.helpers.event import async_track_point_in_time -REQUIREMENTS = ['TwitterAPI==2.5.3'] +REQUIREMENTS = ['TwitterAPI==2.5.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 126d28832b9..d84e0c0d671 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -61,7 +61,7 @@ SoCo==0.14 TravisPy==0.3.5 # homeassistant.components.notify.twitter -TwitterAPI==2.5.3 +TwitterAPI==2.5.4 # homeassistant.components.sensor.waze_travel_time WazeRouteCalculator==0.5 From 2f4c5f949b095172b3bca584dbbe17495a320be5 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 May 2018 20:16:30 +0200 Subject: [PATCH 061/137] Use constants (#14647) --- homeassistant/components/api.py | 140 ++++++++++++++++---------------- 1 file changed, 71 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index dc34006ad03..ae89e2fc3b6 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -2,7 +2,7 @@ Rest API for Home Assistant. For more details about the RESTful API, please refer to the documentation at -https://home-assistant.io/developers/api/ +https://developers.home-assistant.io/docs/en/external_api_rest.html """ import asyncio import json @@ -11,31 +11,34 @@ import logging from aiohttp import web import async_timeout -import homeassistant.core as ha -import homeassistant.remote as rem from homeassistant.bootstrap import DATA_LOGGING -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, - HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND, - MATCH_ALL, URL_API, URL_API_COMPONENTS, - URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, - URL_API_EVENTS, URL_API_SERVICES, - URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE, - __version__) -from homeassistant.exceptions import TemplateError -from homeassistant.helpers.state import AsyncTrackStates -from homeassistant.helpers.service import async_get_all_descriptions -from homeassistant.helpers import template from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST, + HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS, + URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS, + URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, + URL_API_TEMPLATE, __version__) +import homeassistant.core as ha +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template +from homeassistant.helpers.service import async_get_all_descriptions +from homeassistant.helpers.state import AsyncTrackStates +import homeassistant.remote as rem + +_LOGGER = logging.getLogger(__name__) + +ATTR_BASE_URL = 'base_url' +ATTR_LOCATION_NAME = 'location_name' +ATTR_REQUIRES_API_PASSWORD = 'requires_api_password' +ATTR_VERSION = 'version' DOMAIN = 'api' DEPENDENCIES = ['http'] -STREAM_PING_PAYLOAD = "ping" +STREAM_PING_PAYLOAD = 'ping' STREAM_PING_INTERVAL = 50 # seconds -_LOGGER = logging.getLogger(__name__) - def setup(hass, config): """Register the API with the HTTP interface.""" @@ -62,19 +65,19 @@ class APIStatusView(HomeAssistantView): """View to handle Status requests.""" url = URL_API - name = "api:status" + name = 'api:status' @ha.callback def get(self, request): """Retrieve if API is running.""" - return self.json_message('API running.') + return self.json_message("API running.") class APIEventStream(HomeAssistantView): """View to handle EventStream requests.""" url = URL_API_STREAM - name = "api:stream" + name = 'api:stream' async def get(self, request): """Provide a streaming interface for the event bus.""" @@ -95,7 +98,7 @@ class APIEventStream(HomeAssistantView): if restrict and event.event_type not in restrict: return - _LOGGER.debug('STREAM %s FORWARDING %s', id(stop_obj), event) + _LOGGER.debug("STREAM %s FORWARDING %s", id(stop_obj), event) if event.event_type == EVENT_HOMEASSISTANT_STOP: data = stop_obj @@ -111,7 +114,7 @@ class APIEventStream(HomeAssistantView): unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) try: - _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) + _LOGGER.debug("STREAM %s ATTACHED", id(stop_obj)) # Fire off one message so browsers fire open event right away await to_write.put(STREAM_PING_PAYLOAD) @@ -126,25 +129,25 @@ class APIEventStream(HomeAssistantView): break msg = "data: {}\n\n".format(payload) - _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), - msg.strip()) - await response.write(msg.encode("UTF-8")) + _LOGGER.debug( + "STREAM %s WRITING %s", id(stop_obj), msg.strip()) + await response.write(msg.encode('UTF-8')) except asyncio.TimeoutError: await to_write.put(STREAM_PING_PAYLOAD) except asyncio.CancelledError: - _LOGGER.debug('STREAM %s ABORT', id(stop_obj)) + _LOGGER.debug("STREAM %s ABORT", id(stop_obj)) finally: - _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) + _LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj)) unsub_stream() class APIConfigView(HomeAssistantView): - """View to handle Config requests.""" + """View to handle Configuration requests.""" url = URL_API_CONFIG - name = "api:config" + name = 'api:config' @ha.callback def get(self, request): @@ -153,22 +156,22 @@ class APIConfigView(HomeAssistantView): class APIDiscoveryView(HomeAssistantView): - """View to provide discovery info.""" + """View to provide Discovery information.""" requires_auth = False url = URL_API_DISCOVERY_INFO - name = "api:discovery" + name = 'api:discovery' @ha.callback def get(self, request): - """Get discovery info.""" + """Get discovery information.""" hass = request.app['hass'] needs_auth = hass.config.api.api_password is not None return self.json({ - 'base_url': hass.config.api.base_url, - 'location_name': hass.config.location_name, - 'requires_api_password': needs_auth, - 'version': __version__ + ATTR_BASE_URL: hass.config.api.base_url, + ATTR_LOCATION_NAME: hass.config.location_name, + ATTR_REQUIRES_API_PASSWORD: needs_auth, + ATTR_VERSION: __version__, }) @@ -187,8 +190,8 @@ class APIStatesView(HomeAssistantView): class APIEntityStateView(HomeAssistantView): """View to handle EntityState requests.""" - url = "/api/states/{entity_id}" - name = "api:entity-state" + url = '/api/states/{entity_id}' + name = 'api:entity-state' @ha.callback def get(self, request, entity_id): @@ -196,7 +199,7 @@ class APIEntityStateView(HomeAssistantView): state = request.app['hass'].states.get(entity_id) if state: return self.json(state) - return self.json_message('Entity not found', HTTP_NOT_FOUND) + return self.json_message("Entity not found.", HTTP_NOT_FOUND) async def post(self, request, entity_id): """Update state of entity.""" @@ -204,13 +207,13 @@ class APIEntityStateView(HomeAssistantView): try: data = await request.json() except ValueError: - return self.json_message('Invalid JSON specified', - HTTP_BAD_REQUEST) + return self.json_message( + "Invalid JSON specified.", HTTP_BAD_REQUEST) new_state = data.get('state') if new_state is None: - return self.json_message('No state specified', HTTP_BAD_REQUEST) + return self.json_message("No state specified.", HTTP_BAD_REQUEST) attributes = data.get('attributes') force_update = data.get('force_update', False) @@ -232,15 +235,15 @@ class APIEntityStateView(HomeAssistantView): def delete(self, request, entity_id): """Remove entity.""" if request.app['hass'].states.async_remove(entity_id): - return self.json_message('Entity removed') - return self.json_message('Entity not found', HTTP_NOT_FOUND) + return self.json_message("Entity removed.") + return self.json_message("Entity not found.", HTTP_NOT_FOUND) class APIEventListenersView(HomeAssistantView): """View to handle EventListeners requests.""" url = URL_API_EVENTS - name = "api:event-listeners" + name = 'api:event-listeners' @ha.callback def get(self, request): @@ -252,7 +255,7 @@ class APIEventView(HomeAssistantView): """View to handle Event requests.""" url = '/api/events/{event_type}' - name = "api:event" + name = 'api:event' async def post(self, request, event_type): """Fire events.""" @@ -260,12 +263,12 @@ class APIEventView(HomeAssistantView): try: event_data = json.loads(body) if body else None except ValueError: - return self.json_message('Event data should be valid JSON', - HTTP_BAD_REQUEST) + return self.json_message( + "Event data should be valid JSON.", HTTP_BAD_REQUEST) if event_data is not None and not isinstance(event_data, dict): - return self.json_message('Event data should be a JSON object', - HTTP_BAD_REQUEST) + return self.json_message( + "Event data should be a JSON object", HTTP_BAD_REQUEST) # Special case handling for event STATE_CHANGED # We will try to convert state dicts back to State objects @@ -276,8 +279,8 @@ class APIEventView(HomeAssistantView): if state: event_data[key] = state - request.app['hass'].bus.async_fire(event_type, event_data, - ha.EventOrigin.remote) + request.app['hass'].bus.async_fire( + event_type, event_data, ha.EventOrigin.remote) return self.json_message("Event {} fired.".format(event_type)) @@ -286,7 +289,7 @@ class APIServicesView(HomeAssistantView): """View to handle Services requests.""" url = URL_API_SERVICES - name = "api:services" + name = 'api:services' async def get(self, request): """Get registered services.""" @@ -297,8 +300,8 @@ class APIServicesView(HomeAssistantView): class APIDomainServicesView(HomeAssistantView): """View to handle DomainServices requests.""" - url = "/api/services/{domain}/{service}" - name = "api:domain-services" + url = '/api/services/{domain}/{service}' + name = 'api:domain-services' async def post(self, request, domain, service): """Call a service. @@ -310,8 +313,8 @@ class APIDomainServicesView(HomeAssistantView): try: data = json.loads(body) if body else None except ValueError: - return self.json_message('Data should be valid JSON', - HTTP_BAD_REQUEST) + return self.json_message( + "Data should be valid JSON.", HTTP_BAD_REQUEST) with AsyncTrackStates(hass) as changed_states: await hass.services.async_call(domain, service, data, True) @@ -323,7 +326,7 @@ class APIComponentsView(HomeAssistantView): """View to handle Components requests.""" url = URL_API_COMPONENTS - name = "api:components" + name = 'api:components' @ha.callback def get(self, request): @@ -332,10 +335,10 @@ class APIComponentsView(HomeAssistantView): class APITemplateView(HomeAssistantView): - """View to handle requests.""" + """View to handle Template requests.""" url = URL_API_TEMPLATE - name = "api:template" + name = 'api:template' async def post(self, request): """Render a template.""" @@ -344,30 +347,29 @@ class APITemplateView(HomeAssistantView): tpl = template.Template(data['template'], request.app['hass']) return tpl.async_render(data.get('variables')) except (ValueError, TemplateError) as ex: - return self.json_message('Error rendering template: {}'.format(ex), - HTTP_BAD_REQUEST) + return self.json_message( + "Error rendering template: {}".format(ex), HTTP_BAD_REQUEST) class APIErrorLog(HomeAssistantView): - """View to fetch the error log.""" + """View to fetch the API error log.""" url = URL_API_ERROR_LOG - name = "api:error_log" + name = 'api:error_log' async def get(self, request): """Retrieve API error log.""" - return web.FileResponse( - request.app['hass'].data[DATA_LOGGING]) + return web.FileResponse(request.app['hass'].data[DATA_LOGGING]) async def async_services_json(hass): """Generate services data to JSONify.""" descriptions = await async_get_all_descriptions(hass) - return [{"domain": key, "services": value} + return [{'domain': key, 'services': value} for key, value in descriptions.items()] def async_events_json(hass): """Generate event data to JSONify.""" - return [{"event": key, "listener_count": value} + return [{'event': key, 'listener_count': value} for key, value in hass.bus.async_listeners().items()] From 13859388c1895189e756be9393d96d5a2dbf99b4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 May 2018 20:16:47 +0200 Subject: [PATCH 062/137] Upgrade locationsharinglib to 2.0.7 (#14640) --- .../components/device_tracker/google_maps.py | 11 ++++++----- requirements_all.txt | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 3bf0cb0e126..5f06946fc44 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -4,19 +4,20 @@ 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 logging 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, ATTR_ID +from homeassistant.const import ATTR_ID, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType +from homeassistant.util import slugify -REQUIREMENTS = ['locationsharinglib==2.0.2'] +REQUIREMENTS = ['locationsharinglib==2.0.7'] _LOGGER = logging.getLogger(__name__) @@ -70,7 +71,7 @@ class GoogleMapsScanner(object): def _update_info(self, now=None): for person in self.service.get_all_people(): try: - dev_id = 'google_maps_{0}'.format(person.id) + dev_id = 'google_maps_{0}'.format(slugify(person.id)) except TypeError: _LOGGER.warning("No location(s) shared with this account") return diff --git a/requirements_all.txt b/requirements_all.txt index d84e0c0d671..2e944f20132 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -515,7 +515,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==2.0.2 +locationsharinglib==2.0.7 # homeassistant.components.sensor.luftdaten luftdaten==0.2.0 From b6e4a7771a2e3a3658cca547eb1997e93fc64c52 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 May 2018 17:17:19 -0400 Subject: [PATCH 063/137] Allow Hass.io panel dir (#14655) --- homeassistant/components/hassio/http.py | 2 +- tests/components/hassio/test_http.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 9dd6427ec38..bb4f8219a33 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -36,7 +36,7 @@ NO_TIMEOUT = { } NO_AUTH = { - re.compile(r'^app-(es5|latest)/(index|hassio-app).html$'), + re.compile(r'^app-(es5|latest)/.+$'), re.compile(r'^addons/[^/]*/logo$') } diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index ed425ad8cca..ac90deb9f73 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -48,7 +48,7 @@ def test_auth_required_forward_request(hassio_client): @pytest.mark.parametrize( 'build_type', [ 'es5/index.html', 'es5/hassio-app.html', 'latest/index.html', - 'latest/hassio-app.html' + 'latest/hassio-app.html', 'es5/some-chunk.js', 'es5/app.js', ]) def test_forward_request_no_auth_for_panel(hassio_client, build_type): """Test no auth needed for .""" From cc2437614ba5556b517b3897970027bf62d4ad96 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 28 May 2018 08:16:55 +0200 Subject: [PATCH 064/137] Upgrade youtube_dl to 2018.05.26 (#14654) --- 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 bef02d7113f..73837ce2ca1 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.05.18'] +REQUIREMENTS = ['youtube_dl==2018.05.26'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2e944f20132..addc954ba06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1388,7 +1388,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.05.18 +youtube_dl==2018.05.26 # homeassistant.components.light.zengge zengge==0.2 From bff1e1ff6c9bbb223c0df959fd62f78aedb22e2e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 28 May 2018 08:17:10 +0200 Subject: [PATCH 065/137] Upgrade python_opendata_transport to 0.1.0 (#14652) --- .../components/sensor/swiss_public_transport.py | 14 ++++++-------- requirements_all.txt | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index a489adf6776..928d84caa2b 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -4,7 +4,6 @@ Support for transport.opendata.ch. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.swiss_public_transport/ """ -import asyncio from datetime import timedelta import logging @@ -17,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util -REQUIREMENTS = ['python_opendata_transport==0.0.3'] +REQUIREMENTS = ['python_opendata_transport==0.1.0'] _LOGGER = logging.getLogger(__name__) @@ -48,8 +47,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@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 Swiss public transport sensor.""" from opendata_transport import OpendataTransport, exceptions @@ -61,7 +60,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): opendata = OpendataTransport(start, destination, hass.loop, session) try: - yield from opendata.async_get_data() + await opendata.async_get_data() except exceptions.OpendataTransportError: _LOGGER.error( "Check at http://transport.opendata.ch/examples/stationboard.html " @@ -122,12 +121,11 @@ class SwissPublicTransportSensor(Entity): """Icon to use in the frontend, if any.""" return ICON - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from opendata.ch and update the states.""" from opendata_transport.exceptions import OpendataTransportError try: - yield from self._opendata.async_get_data() + await self._opendata.async_get_data() except OpendataTransportError: _LOGGER.error("Unable to retrieve data from transport.opendata.ch") diff --git a/requirements_all.txt b/requirements_all.txt index addc954ba06..11533c195ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1069,7 +1069,7 @@ python-vlc==1.1.2 python-wink==1.7.3 # homeassistant.components.sensor.swiss_public_transport -python_opendata_transport==0.0.3 +python_opendata_transport==0.1.0 # homeassistant.components.zwave python_openzwave==0.4.3 From 799ae894a8c9c641d529625cd18ebe98f9e08bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Mon, 28 May 2018 16:17:01 +0200 Subject: [PATCH 066/137] Remove docker prereqs scripts that only install a package. Add informational message for this. (#14661) --- virtualization/Docker/scripts/ffmpeg | 11 ----------- virtualization/Docker/scripts/iperf3 | 11 ----------- virtualization/Docker/setup_docker_prereqs | 18 ++++++++---------- 3 files changed, 8 insertions(+), 32 deletions(-) delete mode 100755 virtualization/Docker/scripts/ffmpeg delete mode 100755 virtualization/Docker/scripts/iperf3 diff --git a/virtualization/Docker/scripts/ffmpeg b/virtualization/Docker/scripts/ffmpeg deleted file mode 100755 index 914c2648e56..00000000000 --- a/virtualization/Docker/scripts/ffmpeg +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -# Sets up ffmpeg. - -# Stop on errors -set -e - -PACKAGES=( - ffmpeg -) - -apt-get install -y --no-install-recommends ${PACKAGES[@]} diff --git a/virtualization/Docker/scripts/iperf3 b/virtualization/Docker/scripts/iperf3 deleted file mode 100755 index 2d9d5a33761..00000000000 --- a/virtualization/Docker/scripts/iperf3 +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -# Sets up iperf3. - -# Stop on errors -set -e - -PACKAGES=( - iperf3 -) - -apt-get install -y --no-install-recommends ${PACKAGES[@]} diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 97c3c6bdd1c..0cb49fde54e 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -6,11 +6,9 @@ set -e INSTALL_TELLSTICK="${INSTALL_TELLSTICK:-yes}" INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" -INSTALL_FFMPEG="${INSTALL_FFMPEG:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" -INSTALL_IPERF3="${INSTALL_IPERF3:-yes}" # Required debian packages for running hass or components PACKAGES=( @@ -28,6 +26,10 @@ PACKAGES=( libudev-dev # homeassistant.components.homekit_controller libmpc-dev libmpfr-dev libgmp-dev + # homeassistant.components.ffmpeg + ffmpeg + # homeassistant.components.sensor.iperf3 + iperf3 ) # Required debian packages for building dependencies @@ -41,6 +43,10 @@ PACKAGES_DEV=( apt-get update apt-get install -y --no-install-recommends ${PACKAGES[@]} ${PACKAGES_DEV[@]} +# This is a list of scripts that install additional dependencies. If you only +# need to install a package from the official debian repository, just add it +# to the list above. Only create a script if you need compiling, manually +# downloading or a 3th party repository. if [ "$INSTALL_TELLSTICK" == "yes" ]; then virtualization/Docker/scripts/tellstick fi @@ -49,10 +55,6 @@ if [ "$INSTALL_OPENALPR" == "yes" ]; then virtualization/Docker/scripts/openalpr fi -if [ "$INSTALL_FFMPEG" == "yes" ]; then - virtualization/Docker/scripts/ffmpeg -fi - if [ "$INSTALL_LIBCEC" == "yes" ]; then virtualization/Docker/scripts/libcec fi @@ -65,10 +67,6 @@ if [ "$INSTALL_SSOCR" == "yes" ]; then virtualization/Docker/scripts/ssocr fi -if [ "$INSTALL_IPERF3" == "yes" ]; then - virtualization/Docker/scripts/iperf3 -fi - # Remove packages apt-get remove -y --purge ${PACKAGES_DEV[@]} apt-get -y --purge autoremove From 9044a9157ffb12f39e37e4f4f4679b00046b136d Mon Sep 17 00:00:00 2001 From: koreth Date: Mon, 28 May 2018 07:19:03 -0700 Subject: [PATCH 067/137] Reduce log churn from Envisalink binary sensors (#14659) The Envisalink binary sensor was logging events with a relative timestamp that updated every time it polled, so even when nothing new was happening, the event log would be full of meaningless state changes. Modify the sensor code to use an absolute time which stays stable when there isn't new activity. --- .../components/binary_sensor/envisalink.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py index 0aadcc247ea..f358f814dc5 100644 --- a/homeassistant/components/binary_sensor/envisalink.py +++ b/homeassistant/components/binary_sensor/envisalink.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/binary_sensor.envisalink/ """ import asyncio import logging +import datetime from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -14,6 +15,7 @@ from homeassistant.components.envisalink import ( DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice, SIGNAL_ZONE_UPDATE) from homeassistant.const import ATTR_LAST_TRIP_TIME +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -63,7 +65,25 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): def device_state_attributes(self): """Return the state attributes.""" attr = {} - attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault'] + + # The Envisalink library returns a "last_fault" value that's the + # number of seconds since the last fault, up to a maximum of 327680 + # seconds (65536 5-second ticks). + # + # We don't want the HA event log to fill up with a bunch of no-op + # "state changes" that are just that number ticking up once per poll + # interval, so we subtract it from the current second-accurate time + # unless it is already at the maximum value, in which case we set it + # to None since we can't determine the actual value. + seconds_ago = self._info['last_fault'] + if seconds_ago < 65536 * 5: + now = dt_util.now().replace(microsecond=0) + delta = datetime.timedelta(seconds=seconds_ago) + last_trip_time = (now - delta).isoformat() + else: + last_trip_time = None + + attr[ATTR_LAST_TRIP_TIME] = last_trip_time return attr @property From 9a87e62e0ee50a3d464b27a9873298e0856dacaa Mon Sep 17 00:00:00 2001 From: MizterB <5458030+MizterB@users.noreply.github.com> Date: Mon, 28 May 2018 10:21:00 -0400 Subject: [PATCH 068/137] Update Hue platform to aiohue 1.5.0, and re-implement logic for duplicate scene names. (#14653) --- homeassistant/components/hue/__init__.py | 2 +- homeassistant/components/hue/bridge.py | 18 +++++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 0aed854d4e4..251d8cba095 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -17,7 +17,7 @@ from .bridge import HueBridge # Loading the config flow file will register the flow from .config_flow import configured_hosts -REQUIREMENTS = ['aiohue==1.3.0'] +REQUIREMENTS = ['aiohue==1.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 5ff5e2dbf6f..d7a8dc7f730 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -124,9 +124,21 @@ class HueBridge(object): (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) + # The same scene name can exist in multiple groups. + # In this case, activate first scene that contains the + # the exact same light IDs as the group + scenes = [] + for scene in self.api.scenes.values(): + if scene.name == scene_name: + scenes.append(scene) + if len(scenes) == 1: + scene_id = scenes[0].id + else: + group_lights = sorted(group.lights) + for scene in scenes: + if group_lights == scene.lights: + scene_id = scene.id + break # If we can't find it, fetch latest info. if not updated and (group is None or scene_id is None): diff --git a/requirements_all.txt b/requirements_all.txt index 11533c195ce..78239492ce9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -86,7 +86,7 @@ aiodns==1.1.1 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.3.0 +aiohue==1.5.0 # homeassistant.components.sensor.imap aioimaplib==0.7.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f2ed329637..adcba607db0 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.3.0 +aiohue==1.5.0 # homeassistant.components.notify.apns apns2==0.3.0 From 27f3285d17a6303443ac46fd0463745f6c9af64b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 28 May 2018 10:22:29 -0400 Subject: [PATCH 069/137] Force update ZHA electrical sensor (#14649) * force state update because we have a real reading * hound * docstring --- homeassistant/components/sensor/zha.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 984d6efed66..3051d8f2afa 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -198,6 +198,11 @@ class ElectricalMeasurementSensor(Sensor): """Return the unit of measurement of this entity.""" return 'W' + @property + def force_update(self) -> bool: + """Force update this entity.""" + return True + @property def state(self): """Return the state of the entity.""" From 6f4dd7b057b492379404c92b8f96fee20fdc1c74 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 28 May 2018 16:26:33 +0200 Subject: [PATCH 070/137] Improve Homekit media_player options (#14637) * Optimize imports * Optimize name * Optimize config schema * Rename mode to feature * Replace mode with feature_list --- homeassistant/components/homekit/__init__.py | 26 ++--- homeassistant/components/homekit/const.py | 14 ++- .../components/homekit/type_media_players.py | 96 ++++++++-------- homeassistant/components/homekit/util.py | 107 +++++++++++------- .../homekit/test_get_accessories.py | 32 +++--- .../homekit/test_type_media_players.py | 55 +++++---- tests/components/homekit/test_util.py | 34 +++--- 7 files changed, 201 insertions(+), 163 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index f011a56a77b..a79fbf85400 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -9,12 +9,11 @@ from zlib import adler32 import voluptuous as vol -from homeassistant.components.cover import ( - SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) +import homeassistant.components.cover as cover from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - CONF_IP_ADDRESS, CONF_MODE, CONF_NAME, CONF_PORT, - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv @@ -22,11 +21,11 @@ from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry from .const import ( - CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START, - DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, - SERVICE_HOMEKIT_START) + CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, CONF_FILTER, + DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, + DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START) from .util import ( - show_setup_message, validate_entity_config, validate_media_player_modes) + show_setup_message, validate_entity_config, validate_media_player_features) TYPES = Registry() _LOGGER = logging.getLogger(__name__) @@ -110,11 +109,11 @@ def get_accessory(hass, state, aid, config): device_class = state.attributes.get(ATTR_DEVICE_CLASS) if device_class == 'garage' and \ - features & (SUPPORT_OPEN | SUPPORT_CLOSE): + features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): a_type = 'GarageDoorOpener' - elif features & SUPPORT_SET_POSITION: + elif features & cover.SUPPORT_SET_POSITION: a_type = 'WindowCovering' - elif features & (SUPPORT_OPEN | SUPPORT_CLOSE): + elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): a_type = 'WindowCoveringBasic' elif state.domain == 'fan': @@ -127,8 +126,9 @@ def get_accessory(hass, state, aid, config): a_type = 'Lock' elif state.domain == 'media_player': - validate_media_player_modes(state, config) - if config.get(CONF_MODE): + feature_list = config.get(CONF_FEATURE_LIST) + if feature_list and \ + validate_media_player_features(state, feature_list): a_type = 'MediaPlayer' elif state.domain == 'sensor': diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index f59ee5488ec..6d49c806e0f 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -8,12 +8,20 @@ HOMEKIT_NOTIFY_ID = 4663548 # #### Config #### CONF_AUTO_START = 'auto_start' CONF_ENTITY_CONFIG = 'entity_config' +CONF_FEATURE = 'feature' +CONF_FEATURE_LIST = 'feature_list' CONF_FILTER = 'filter' # #### Config Defaults #### DEFAULT_AUTO_START = True DEFAULT_PORT = 51827 +# #### Features #### +FEATURE_ON_OFF = 'on_off' +FEATURE_PLAY_PAUSE = 'play_pause' +FEATURE_PLAY_STOP = 'play_stop' +FEATURE_TOGGLE_MUTE = 'toggle_mute' + # #### HomeKit Component Services #### SERVICE_HOMEKIT_START = 'start' @@ -23,12 +31,6 @@ BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' MANUFACTURER = 'Home Assistant' -# #### Media Player Modes #### -ON_OFF = 'on_off' -PLAY_PAUSE = 'play_pause' -PLAY_STOP = 'play_stop' -TOGGLE_MUTE = 'toggle_mute' - # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 563cd0cb25c..ec41b9fd618 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -4,7 +4,7 @@ import logging from pyhap.const import CATEGORY_SWITCH from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_MODE, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + ATTR_ENTITY_ID, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, STATE_OFF, STATE_PLAYING, STATE_UNKNOWN) from homeassistant.components.media_player import ( @@ -13,15 +13,15 @@ from homeassistant.components.media_player import ( from . import TYPES from .accessories import HomeAccessory from .const import ( - CHAR_NAME, CHAR_ON, ON_OFF, PLAY_PAUSE, PLAY_STOP, SERV_SWITCH, - TOGGLE_MUTE) + CHAR_NAME, CHAR_ON, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, + FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, SERV_SWITCH) _LOGGER = logging.getLogger(__name__) -MODE_FRIENDLY_NAME = {ON_OFF: 'Power', - PLAY_PAUSE: 'Play/Pause', - PLAY_STOP: 'Play/Stop', - TOGGLE_MUTE: 'Mute'} +MODE_FRIENDLY_NAME = {FEATURE_ON_OFF: 'Power', + FEATURE_PLAY_PAUSE: 'Play/Pause', + FEATURE_PLAY_STOP: 'Play/Stop', + FEATURE_TOGGLE_MUTE: 'Mute'} @TYPES.register('MediaPlayer') @@ -31,38 +31,38 @@ class MediaPlayer(HomeAccessory): def __init__(self, *args): """Initialize a Switch accessory object.""" super().__init__(*args, category=CATEGORY_SWITCH) - self._flag = {ON_OFF: False, PLAY_PAUSE: False, - PLAY_STOP: False, TOGGLE_MUTE: False} - self.chars = {ON_OFF: None, PLAY_PAUSE: None, - PLAY_STOP: None, TOGGLE_MUTE: None} - modes = self.config[CONF_MODE] + self._flag = {FEATURE_ON_OFF: False, FEATURE_PLAY_PAUSE: False, + FEATURE_PLAY_STOP: False, FEATURE_TOGGLE_MUTE: False} + self.chars = {FEATURE_ON_OFF: None, FEATURE_PLAY_PAUSE: None, + FEATURE_PLAY_STOP: None, FEATURE_TOGGLE_MUTE: None} + feature_list = self.config[CONF_FEATURE_LIST] - if ON_OFF in modes: + if FEATURE_ON_OFF in feature_list: + name = self.generate_service_name(FEATURE_ON_OFF) serv_on_off = self.add_preload_service(SERV_SWITCH, CHAR_NAME) - serv_on_off.configure_char( - CHAR_NAME, value=self.generate_service_name(ON_OFF)) - self.chars[ON_OFF] = serv_on_off.configure_char( + serv_on_off.configure_char(CHAR_NAME, value=name) + self.chars[FEATURE_ON_OFF] = serv_on_off.configure_char( CHAR_ON, value=False, setter_callback=self.set_on_off) - if PLAY_PAUSE in modes: + if FEATURE_PLAY_PAUSE in feature_list: + name = self.generate_service_name(FEATURE_PLAY_PAUSE) serv_play_pause = self.add_preload_service(SERV_SWITCH, CHAR_NAME) - serv_play_pause.configure_char( - CHAR_NAME, value=self.generate_service_name(PLAY_PAUSE)) - self.chars[PLAY_PAUSE] = serv_play_pause.configure_char( + serv_play_pause.configure_char(CHAR_NAME, value=name) + self.chars[FEATURE_PLAY_PAUSE] = serv_play_pause.configure_char( CHAR_ON, value=False, setter_callback=self.set_play_pause) - if PLAY_STOP in modes: + if FEATURE_PLAY_STOP in feature_list: + name = self.generate_service_name(FEATURE_PLAY_STOP) serv_play_stop = self.add_preload_service(SERV_SWITCH, CHAR_NAME) - serv_play_stop.configure_char( - CHAR_NAME, value=self.generate_service_name(PLAY_STOP)) - self.chars[PLAY_STOP] = serv_play_stop.configure_char( + serv_play_stop.configure_char(CHAR_NAME, value=name) + self.chars[FEATURE_PLAY_STOP] = serv_play_stop.configure_char( CHAR_ON, value=False, setter_callback=self.set_play_stop) - if TOGGLE_MUTE in modes: + if FEATURE_TOGGLE_MUTE in feature_list: + name = self.generate_service_name(FEATURE_TOGGLE_MUTE) serv_toggle_mute = self.add_preload_service(SERV_SWITCH, CHAR_NAME) - serv_toggle_mute.configure_char( - CHAR_NAME, value=self.generate_service_name(TOGGLE_MUTE)) - self.chars[TOGGLE_MUTE] = serv_toggle_mute.configure_char( + serv_toggle_mute.configure_char(CHAR_NAME, value=name) + self.chars[FEATURE_TOGGLE_MUTE] = serv_toggle_mute.configure_char( CHAR_ON, value=False, setter_callback=self.set_toggle_mute) def generate_service_name(self, mode): @@ -73,7 +73,7 @@ class MediaPlayer(HomeAccessory): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_id, value) - self._flag[ON_OFF] = True + self._flag[FEATURE_ON_OFF] = True service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF params = {ATTR_ENTITY_ID: self.entity_id} self.hass.services.call(DOMAIN, service, params) @@ -82,7 +82,7 @@ class MediaPlayer(HomeAccessory): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state for "play_pause" to %s', self.entity_id, value) - self._flag[PLAY_PAUSE] = True + self._flag[FEATURE_PLAY_PAUSE] = True service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE params = {ATTR_ENTITY_ID: self.entity_id} self.hass.services.call(DOMAIN, service, params) @@ -91,7 +91,7 @@ class MediaPlayer(HomeAccessory): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state for "play_stop" to %s', self.entity_id, value) - self._flag[PLAY_STOP] = True + self._flag[FEATURE_PLAY_STOP] = True service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP params = {ATTR_ENTITY_ID: self.entity_id} self.hass.services.call(DOMAIN, service, params) @@ -100,7 +100,7 @@ class MediaPlayer(HomeAccessory): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state for "toggle_mute" to %s', self.entity_id, value) - self._flag[TOGGLE_MUTE] = True + self._flag[FEATURE_TOGGLE_MUTE] = True params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_MUTED: value} self.hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, params) @@ -109,34 +109,34 @@ class MediaPlayer(HomeAccessory): """Update switch state after state changed.""" current_state = new_state.state - if self.chars[ON_OFF]: + if self.chars[FEATURE_ON_OFF]: hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN, 'None') - if not self._flag[ON_OFF]: + if not self._flag[FEATURE_ON_OFF]: _LOGGER.debug('%s: Set current state for "on_off" to %s', self.entity_id, hk_state) - self.chars[ON_OFF].set_value(hk_state) - self._flag[ON_OFF] = False + self.chars[FEATURE_ON_OFF].set_value(hk_state) + self._flag[FEATURE_ON_OFF] = False - if self.chars[PLAY_PAUSE]: + if self.chars[FEATURE_PLAY_PAUSE]: hk_state = current_state == STATE_PLAYING - if not self._flag[PLAY_PAUSE]: + if not self._flag[FEATURE_PLAY_PAUSE]: _LOGGER.debug('%s: Set current state for "play_pause" to %s', self.entity_id, hk_state) - self.chars[PLAY_PAUSE].set_value(hk_state) - self._flag[PLAY_PAUSE] = False + self.chars[FEATURE_PLAY_PAUSE].set_value(hk_state) + self._flag[FEATURE_PLAY_PAUSE] = False - if self.chars[PLAY_STOP]: + if self.chars[FEATURE_PLAY_STOP]: hk_state = current_state == STATE_PLAYING - if not self._flag[PLAY_STOP]: + if not self._flag[FEATURE_PLAY_STOP]: _LOGGER.debug('%s: Set current state for "play_stop" to %s', self.entity_id, hk_state) - self.chars[PLAY_STOP].set_value(hk_state) - self._flag[PLAY_STOP] = False + self.chars[FEATURE_PLAY_STOP].set_value(hk_state) + self._flag[FEATURE_PLAY_STOP] = False - if self.chars[TOGGLE_MUTE]: + if self.chars[FEATURE_TOGGLE_MUTE]: current_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) - if not self._flag[TOGGLE_MUTE]: + if not self._flag[FEATURE_TOGGLE_MUTE]: _LOGGER.debug('%s: Set current state for "toggle_mute" to %s', self.entity_id, current_state) - self.chars[TOGGLE_MUTE].set_value(current_state) - self._flag[TOGGLE_MUTE] = False + self.chars[FEATURE_TOGGLE_MUTE].set_value(current_state) + self._flag[FEATURE_TOGGLE_MUTE] = False diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 57ce562ce21..50095844757 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -3,20 +3,37 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import ( - SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_STOP, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE) +import homeassistant.components.media_player as media_player from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_MODE, CONF_NAME, TEMP_CELSIUS) + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util from .const import ( - HOMEKIT_NOTIFY_ID, ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) + CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, + FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE) _LOGGER = logging.getLogger(__name__) -MEDIA_PLAYER_MODES = (ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) + +BASIC_INFO_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME): cv.string, +}) + +FEATURE_SCHEMA = BASIC_INFO_SCHEMA.extend({ + vol.Optional(CONF_FEATURE_LIST, default=None): cv.ensure_list, +}) + + +CODE_SCHEMA = BASIC_INFO_SCHEMA.extend({ + vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string), +}) + +MEDIA_PLAYER_SCHEMA = vol.Schema({ + vol.Required(CONF_FEATURE): vol.All( + cv.string, vol.In((FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, + FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE))), +}) def validate_entity_config(values): @@ -24,57 +41,59 @@ def validate_entity_config(values): entities = {} for entity_id, config in values.items(): entity = cv.entity_id(entity_id) - params = {} - if not isinstance(config, dict): - raise vol.Invalid('The configuration for "{}" must be ' - ' a dictionary.'.format(entity)) - - for key in (CONF_NAME, ): - value = config.get(key, -1) - if value != -1: - params[key] = cv.string(value) - domain, _ = split_entity_id(entity) + if not isinstance(config, dict): + raise vol.Invalid('The configuration for {} must be ' + ' a dictionary.'.format(entity)) + if domain in ('alarm_control_panel', 'lock'): - code = config.get(ATTR_CODE) - params[ATTR_CODE] = cv.string(code) if code else None + config = CODE_SCHEMA(config) - if domain == 'media_player': - mode = config.get(CONF_MODE) - params[CONF_MODE] = cv.ensure_list(mode) - for key in params[CONF_MODE]: - if key not in MEDIA_PLAYER_MODES: - raise vol.Invalid( - 'Invalid mode: "{}", valid modes are: "{}".' - .format(key, MEDIA_PLAYER_MODES)) + elif domain == media_player.DOMAIN: + config = FEATURE_SCHEMA(config) + feature_list = {} + for feature in config[CONF_FEATURE_LIST]: + params = MEDIA_PLAYER_SCHEMA(feature) + key = params.pop(CONF_FEATURE) + if key in feature_list: + raise vol.Invalid('A feature can be added only once for {}' + .format(entity)) + feature_list[key] = params + config[CONF_FEATURE_LIST] = feature_list - entities[entity] = params + else: + config = BASIC_INFO_SCHEMA(config) + + entities[entity] = config return entities -def validate_media_player_modes(state, config): - """Validate modes for media playeres.""" +def validate_media_player_features(state, feature_list): + """Validate features for media players.""" features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported_modes = [] - if features & (SUPPORT_TURN_ON | SUPPORT_TURN_OFF): - supported_modes.append(ON_OFF) - if features & (SUPPORT_PLAY | SUPPORT_PAUSE): - supported_modes.append(PLAY_PAUSE) - if features & (SUPPORT_PLAY | SUPPORT_STOP): - supported_modes.append(PLAY_STOP) - if features & SUPPORT_VOLUME_MUTE: - supported_modes.append(TOGGLE_MUTE) + if features & (media_player.SUPPORT_TURN_ON | + media_player.SUPPORT_TURN_OFF): + supported_modes.append(FEATURE_ON_OFF) + if features & (media_player.SUPPORT_PLAY | media_player.SUPPORT_PAUSE): + supported_modes.append(FEATURE_PLAY_PAUSE) + if features & (media_player.SUPPORT_PLAY | media_player.SUPPORT_STOP): + supported_modes.append(FEATURE_PLAY_STOP) + if features & media_player.SUPPORT_VOLUME_MUTE: + supported_modes.append(FEATURE_TOGGLE_MUTE) - if not config.get(CONF_MODE): - config[CONF_MODE] = supported_modes - return + error_list = [] + for feature in feature_list: + if feature not in supported_modes: + error_list.append(feature) - for mode in config[CONF_MODE]: - if mode not in supported_modes: - raise vol.Invalid('"{}" does not support mode: "{}".' - .format(state.entity_id, mode)) + if error_list: + _LOGGER.error("%s does not support features: %s", + state.entity_id, error_list) + return False + return True def show_setup_message(hass, pincode): diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 11b2d737a70..46e5f8b1174 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -2,20 +2,17 @@ from unittest.mock import patch, Mock import pytest -import voluptuous as vol from homeassistant.core import State -from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN -from homeassistant.components.climate import ( - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) -from homeassistant.components.media_player import ( - SUPPORT_TURN_OFF, SUPPORT_TURN_ON) +import homeassistant.components.cover as cover +import homeassistant.components.climate as climate +import homeassistant.components.media_player as media_player from homeassistant.components.homekit import get_accessory, TYPES -from homeassistant.components.homekit.const import ON_OFF +from homeassistant.components.homekit.const import ( + CONF_FEATURE_LIST, FEATURE_ON_OFF) from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, CONF_MODE, CONF_NAME, TEMP_CELSIUS, - TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) def test_not_supported(caplog): @@ -32,9 +29,9 @@ def test_not_supported(caplog): def test_not_supported_media_player(): """Test if mode isn't supported and if no supported modes.""" # selected mode for entity not supported - with pytest.raises(vol.Invalid): - entity_state = State('media_player.demo', 'on') - get_accessory(None, entity_state, 2, {CONF_MODE: [ON_OFF]}) + config = {CONF_FEATURE_LIST: {FEATURE_ON_OFF: None}} + entity_state = State('media_player.demo', 'on') + get_accessory(None, entity_state, 2, config) is None # no supported modes for entity entity_state = State('media_player.demo', 'on') @@ -58,14 +55,15 @@ def test_customize_options(config, name): ('Light', 'light.test', 'on', {}, {}), ('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}), ('MediaPlayer', 'media_player.test', 'on', - {ATTR_SUPPORTED_FEATURES: SUPPORT_TURN_ON | SUPPORT_TURN_OFF}, - {CONF_MODE: [ON_OFF]}), + {ATTR_SUPPORTED_FEATURES: media_player.SUPPORT_TURN_ON | + media_player.SUPPORT_TURN_OFF}, {CONF_FEATURE_LIST: + {FEATURE_ON_OFF: None}}), ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), ('Thermostat', 'climate.test', 'auto', - {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_LOW | - SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), + {ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE_LOW | + climate.SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), ]) def test_types(type_name, entity_id, state, attrs, config): """Test if types are associated correctly.""" @@ -82,7 +80,7 @@ def test_types(type_name, entity_id, state, attrs, config): @pytest.mark.parametrize('type_name, entity_id, state, attrs', [ ('GarageDoorOpener', 'cover.garage_door', 'open', {ATTR_DEVICE_CLASS: 'garage', - ATTR_SUPPORTED_FEATURES: SUPPORT_OPEN | SUPPORT_CLOSE}), + ATTR_SUPPORTED_FEATURES: cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE}), ('WindowCovering', 'cover.set_position', 'open', {ATTR_SUPPORTED_FEATURES: 4}), ('WindowCoveringBasic', 'cover.open_window', 'open', diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 03135b1418e..d89f9740ea6 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -4,9 +4,10 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_VOLUME_MUTED, DOMAIN) from homeassistant.components.homekit.type_media_players import MediaPlayer from homeassistant.components.homekit.const import ( - ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) + CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, + FEATURE_TOGGLE_MUTE) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_MODE, SERVICE_MEDIA_PAUSE, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) @@ -16,7 +17,9 @@ from tests.common import async_mock_service async def test_media_player_set_state(hass): """Test if accessory and HA are updated accordingly.""" - config = {CONF_MODE: [ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE]} + config = {CONF_FEATURE_LIST: { + FEATURE_ON_OFF: None, FEATURE_PLAY_PAUSE: None, + FEATURE_PLAY_STOP: None, FEATURE_TOGGLE_MUTE: None}} entity_id = 'media_player.test' hass.states.async_set(entity_id, None, {ATTR_SUPPORTED_FEATURES: 20873, @@ -28,32 +31,32 @@ async def test_media_player_set_state(hass): assert acc.aid == 2 assert acc.category == 8 # Switch - assert acc.chars[ON_OFF].value == 0 - assert acc.chars[PLAY_PAUSE].value == 0 - assert acc.chars[PLAY_STOP].value == 0 - assert acc.chars[TOGGLE_MUTE].value == 0 + assert acc.chars[FEATURE_ON_OFF].value == 0 + assert acc.chars[FEATURE_PLAY_PAUSE].value == 0 + assert acc.chars[FEATURE_PLAY_STOP].value == 0 + assert acc.chars[FEATURE_TOGGLE_MUTE].value == 0 hass.states.async_set(entity_id, STATE_ON, {ATTR_MEDIA_VOLUME_MUTED: True}) await hass.async_block_till_done() - assert acc.chars[ON_OFF].value == 1 - assert acc.chars[TOGGLE_MUTE].value == 1 + assert acc.chars[FEATURE_ON_OFF].value == 1 + assert acc.chars[FEATURE_TOGGLE_MUTE].value == 1 hass.states.async_set(entity_id, STATE_OFF) await hass.async_block_till_done() - assert acc.chars[ON_OFF].value == 0 + assert acc.chars[FEATURE_ON_OFF].value == 0 hass.states.async_set(entity_id, STATE_PLAYING) await hass.async_block_till_done() - assert acc.chars[PLAY_PAUSE].value == 1 - assert acc.chars[PLAY_STOP].value == 1 + assert acc.chars[FEATURE_PLAY_PAUSE].value == 1 + assert acc.chars[FEATURE_PLAY_STOP].value == 1 hass.states.async_set(entity_id, STATE_PAUSED) await hass.async_block_till_done() - assert acc.chars[PLAY_PAUSE].value == 0 + assert acc.chars[FEATURE_PLAY_PAUSE].value == 0 hass.states.async_set(entity_id, STATE_IDLE) await hass.async_block_till_done() - assert acc.chars[PLAY_STOP].value == 0 + assert acc.chars[FEATURE_PLAY_STOP].value == 0 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) @@ -63,43 +66,51 @@ async def test_media_player_set_state(hass): call_media_stop = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_STOP) call_toggle_mute = async_mock_service(hass, DOMAIN, SERVICE_VOLUME_MUTE) - await hass.async_add_job(acc.chars[ON_OFF].client_update_value, True) + await hass.async_add_job(acc.chars[FEATURE_ON_OFF] + .client_update_value, True) await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id - await hass.async_add_job(acc.chars[ON_OFF].client_update_value, False) + await hass.async_add_job(acc.chars[FEATURE_ON_OFF] + .client_update_value, False) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id - await hass.async_add_job(acc.chars[PLAY_PAUSE].client_update_value, True) + await hass.async_add_job(acc.chars[FEATURE_PLAY_PAUSE] + .client_update_value, True) await hass.async_block_till_done() assert call_media_play assert call_media_play[0].data[ATTR_ENTITY_ID] == entity_id - await hass.async_add_job(acc.chars[PLAY_PAUSE].client_update_value, False) + await hass.async_add_job(acc.chars[FEATURE_PLAY_PAUSE] + .client_update_value, False) await hass.async_block_till_done() assert call_media_pause assert call_media_pause[0].data[ATTR_ENTITY_ID] == entity_id - await hass.async_add_job(acc.chars[PLAY_STOP].client_update_value, True) + await hass.async_add_job(acc.chars[FEATURE_PLAY_STOP] + .client_update_value, True) await hass.async_block_till_done() assert call_media_play assert call_media_play[1].data[ATTR_ENTITY_ID] == entity_id - await hass.async_add_job(acc.chars[PLAY_STOP].client_update_value, False) + await hass.async_add_job(acc.chars[FEATURE_PLAY_STOP] + .client_update_value, False) await hass.async_block_till_done() assert call_media_stop assert call_media_stop[0].data[ATTR_ENTITY_ID] == entity_id - await hass.async_add_job(acc.chars[TOGGLE_MUTE].client_update_value, True) + await hass.async_add_job(acc.chars[FEATURE_TOGGLE_MUTE] + .client_update_value, True) await hass.async_block_till_done() assert call_toggle_mute assert call_toggle_mute[0].data[ATTR_ENTITY_ID] == entity_id assert call_toggle_mute[0].data[ATTR_MEDIA_VOLUME_MUTED] is True - await hass.async_add_job(acc.chars[TOGGLE_MUTE].client_update_value, False) + await hass.async_add_job(acc.chars[FEATURE_TOGGLE_MUTE] + .client_update_value, False) await hass.async_block_till_done() assert call_toggle_mute assert call_toggle_mute[1].data[ATTR_ENTITY_ID] == entity_id diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 56a625e02d7..0bc1eb96841 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -4,17 +4,18 @@ import voluptuous as vol from homeassistant.core import State from homeassistant.components.homekit.const import ( - HOMEKIT_NOTIFY_ID, ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) + CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, + FEATURE_PLAY_PAUSE) from homeassistant.components.homekit.util import ( convert_to_float, density_to_air_quality, dismiss_setup_message, show_setup_message, temperature_to_homekit, temperature_to_states, - validate_media_player_modes) + validate_media_player_features) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN) from homeassistant.const import ( - ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_MODE, CONF_NAME, STATE_UNKNOWN, + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service @@ -25,7 +26,11 @@ def test_validate_entity_config(): configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, {'demo.test': 'test'}, {'demo.test': [1, 2]}, {'demo.test': None}, {'demo.test': {CONF_NAME: None}}, - {'media_player.test': {CONF_MODE: 'invalid_mode'}}] + {'media_player.test': {CONF_FEATURE_LIST: [ + {CONF_FEATURE: 'invalid_feature'}]}}, + {'media_player.test': {CONF_FEATURE_LIST: [ + {CONF_FEATURE: FEATURE_ON_OFF}, + {CONF_FEATURE: FEATURE_ON_OFF}]}}, ] for conf in configs: with pytest.raises(vol.Invalid): @@ -45,23 +50,26 @@ def test_validate_entity_config(): {'lock.demo': {ATTR_CODE: '1234'}} assert vec({'media_player.demo': {}}) == \ - {'media_player.demo': {CONF_MODE: []}} - assert vec({'media_player.demo': {CONF_MODE: [ON_OFF]}}) == \ - {'media_player.demo': {CONF_MODE: [ON_OFF]}} + {'media_player.demo': {CONF_FEATURE_LIST: {}}} + config = {CONF_FEATURE_LIST: [{CONF_FEATURE: FEATURE_ON_OFF}, + {CONF_FEATURE: FEATURE_PLAY_PAUSE}]} + assert vec({'media_player.demo': config}) == \ + {'media_player.demo': {CONF_FEATURE_LIST: + {FEATURE_ON_OFF: {}, FEATURE_PLAY_PAUSE: {}}}} -def test_validate_media_player_modes(): +def test_validate_media_player_features(): """Test validate modes for media players.""" config = {} attrs = {ATTR_SUPPORTED_FEATURES: 20873} entity_state = State('media_player.demo', 'on', attrs) - validate_media_player_modes(entity_state, config) - assert config == {CONF_MODE: [ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE]} + assert validate_media_player_features(entity_state, config) is True + + config = {FEATURE_ON_OFF: None} + assert validate_media_player_features(entity_state, config) is True entity_state = State('media_player.demo', 'on') - config = {CONF_MODE: [ON_OFF]} - with pytest.raises(vol.Invalid): - validate_media_player_modes(entity_state, config) + assert validate_media_player_features(entity_state, config) is False def test_convert_to_float(): From 144bb3492ad260f77ef1374d2de15a07c0b04f57 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 28 May 2018 10:32:47 -0400 Subject: [PATCH 071/137] zha/light: Properly parse currentX and currentY on async_update() (#14605) --- homeassistant/components/light/zha.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index b44bf820b23..bd01a513e0b 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -172,7 +172,8 @@ class Light(zha.Entity, light.Light): result = await zha.safe_read(self._endpoint.light_color, ['current_x', 'current_y']) if 'current_x' in result and 'current_y' in result: - xy_color = (result['current_x'], result['current_y']) + xy_color = (round(result['current_x']/65535, 3), + round(result['current_y']/65535, 3)) self._hs_color = color_util.color_xy_to_hs(*xy_color) @property From 07255a29b4c44a6ece10a20c91b062dd906f0cbe Mon Sep 17 00:00:00 2001 From: Enrico Berndt Date: Mon, 28 May 2018 16:41:51 +0200 Subject: [PATCH 072/137] Add tv channel and volume level for philips js API 5 (#14276) * PhilipsTV API 5: Added tv channel change and setting of volume level. * set_volume only sets volume via api now and nothing else. --- .../components/media_player/philips_js.py | 20 +++++++++++-------- requirements_all.txt | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index d526fbb0387..01d63e0b6c8 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -13,20 +13,22 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY, MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + SUPPORT_PLAY, MediaPlayerDevice) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_API_VERSION, STATE_OFF, STATE_ON, STATE_UNKNOWN) from homeassistant.helpers.script import Script from homeassistant.util import Throttle -REQUIREMENTS = ['ha-philipsjs==0.0.3'] +REQUIREMENTS = ['ha-philipsjs==0.0.4'] _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ - SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE + SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_SELECT_SOURCE SUPPORT_PHILIPS_JS_TV = SUPPORT_PHILIPS_JS | SUPPORT_NEXT_TRACK | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY @@ -165,6 +167,10 @@ class PhilipsTV(MediaPlayerDevice): if not self._tv.on: self._state = STATE_OFF + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._tv.setVolume(volume) + def media_previous_track(self): """Send rewind command.""" self._tv.sendKey('Previous') @@ -189,12 +195,10 @@ class PhilipsTV(MediaPlayerDevice): self._volume = self._tv.volume self._muted = self._tv.muted if self._tv.source_id: - src = self._tv.sources.get(self._tv.source_id, None) - if src: - self._source = src.get('name', None) + self._source = self._tv.getSourceName(self._tv.source_id) if self._tv.sources and not self._source_list: - for srcid in sorted(self._tv.sources): - srcname = self._tv.sources.get(srcid, dict()).get('name', None) + for srcid in self._tv.sources: + srcname = self._tv.getSourceName(srcid) self._source_list.append(srcname) self._source_mapping[srcname] = srcid if self._tv.on: diff --git a/requirements_all.txt b/requirements_all.txt index 78239492ce9..ef4914a5ec0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -368,7 +368,7 @@ gstreamer-player==1.1.0 ha-ffmpeg==1.9 # homeassistant.components.media_player.philips_js -ha-philipsjs==0.0.3 +ha-philipsjs==0.0.4 # homeassistant.components.sensor.geo_rss_events haversine==0.4.5 From 6c3e2021df523be5f47f23dfead4f0d0cb565a92 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 29 May 2018 04:49:38 +0300 Subject: [PATCH 073/137] Give unknown zwave nodes a better name (#14353) * Give unknown zwave nodes a better name * Update util.py --- homeassistant/components/zwave/util.py | 8 +++++--- tests/components/zwave/test_init.py | 5 +++-- tests/components/zwave/test_node_entity.py | 1 + 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py index 1c0bb14f7e5..b62eeb67d32 100644 --- a/homeassistant/components/zwave/util.py +++ b/homeassistant/components/zwave/util.py @@ -68,8 +68,10 @@ def check_value_schema(value, schema): def node_name(node): """Return the name of the node.""" - return node.name or '{} {}'.format( - node.manufacturer_name, node.product_name) + if is_node_parsed(node): + return node.name or '{} {}'.format( + node.manufacturer_name, node.product_name) + return 'Unknown Node {}'.format(node.node_id) async def check_has_unique_id(entity, ready_callback, timeout_callback, loop): @@ -89,4 +91,4 @@ async def check_has_unique_id(entity, ready_callback, timeout_callback, loop): def is_node_parsed(node): """Check whether the node has been parsed or still waiting to be parsed.""" - return node.manufacturer_name and node.product_name + return bool((node.manufacturer_name and node.product_name) or node.name) diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index a25b725e500..e608dcccaba 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -238,7 +238,8 @@ async def test_unparsed_node_discovery(hass, mock_openzwave): assert len(mock_receivers) == 1 - node = MockNode(node_id=14, manufacturer_name=None, is_ready=False) + node = MockNode( + node_id=14, manufacturer_name=None, name=None, is_ready=False) sleeps = [] @@ -263,7 +264,7 @@ async def test_unparsed_node_discovery(hass, mock_openzwave): assert len(mock_logger.warning.mock_calls) == 1 assert mock_logger.warning.mock_calls[0][1][1:] == \ (14, const.NODE_READY_WAIT_SECS) - assert hass.states.get('zwave.mock_node').state is 'unknown' + assert hass.states.get('zwave.unknown_node_14').state is 'unknown' @asyncio.coroutine diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index f4d9b3ef0e8..b91245d5a12 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -363,6 +363,7 @@ class TestZWaveNodeEntity(unittest.TestCase): def test_unique_id_missing_data(self): """Test unique_id.""" self.node.manufacturer_name = None + self.node.name = None entity = node_entity.ZWaveNodeEntity(self.node, self.zwave_network) self.assertIsNone(entity.unique_id) From 9bc26e93a4340b4a4e2eb35f4732a6b60d435f43 Mon Sep 17 00:00:00 2001 From: Robert Accettura Date: Tue, 29 May 2018 01:50:27 -0400 Subject: [PATCH 074/137] Add pin pad to alarm panel (#14178) * Add pin pad to alarm panel * Add pin pad to alarm panel * Update regex * Update regex * Update regex * Add pin pad to alarm panel * Add pin pad to alarm panel * Add pin pad to alarm panel * Add pin pad to alarm panel * Fix typos * Fix typos * Fix typos * Add pin pad to alarm panel * Fix errors --- .../components/alarm_control_panel/alarmdecoder.py | 4 ++-- .../components/alarm_control_panel/alarmdotcom.py | 9 +++++++-- .../components/alarm_control_panel/concord232.py | 2 +- .../components/alarm_control_panel/envisalink.py | 2 +- homeassistant/components/alarm_control_panel/ifttt.py | 9 +++++++-- homeassistant/components/alarm_control_panel/manual.py | 9 +++++++-- .../components/alarm_control_panel/manual_mqtt.py | 9 +++++++-- homeassistant/components/alarm_control_panel/mqtt.py | 9 +++++++-- homeassistant/components/alarm_control_panel/nx584.py | 4 ++-- .../components/alarm_control_panel/simplisafe.py | 9 +++++++-- homeassistant/components/alarm_control_panel/verisure.py | 4 ++-- 11 files changed, 50 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py index 49df9f2cefa..13b51aea701 100644 --- a/homeassistant/components/alarm_control_panel/alarmdecoder.py +++ b/homeassistant/components/alarm_control_panel/alarmdecoder.py @@ -100,8 +100,8 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): - """Return the regex for code format or None if no code is required.""" - return '^\\d{4,6}$' + """Return one or more digits/characters.""" + return '^\\d+$' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 31d93373286..6b523e8b606 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/alarm_control_panel.alarmdotcom/ """ import asyncio import logging +import re import voluptuous as vol @@ -79,8 +80,12 @@ class AlarmDotCom(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters if code is defined.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return '^\\d+$' + return '.+' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py index d48a107f33d..bd3ee762ccb 100644 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -80,7 +80,7 @@ class Concord232Alarm(alarm.AlarmControlPanel): @property def code_format(self): """Return the characters if code is defined.""" - return '[0-9]{4}([0-9]{2})?' + return '^\\d+$' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index e5003f1ba1d..19bbfa611f2 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -106,7 +106,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Regex for code format or None if no code is required.""" if self._code: return None - return '^\\d{4,6}$' + return '^\\d+$' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py index 7bdc1ccd9d9..203044f3915 100644 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.ifttt/ """ import logging +import re import voluptuous as vol @@ -124,8 +125,12 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return '^\\d+$' + return '.+' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 5beb5261607..e66251143da 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/alarm_control_panel.manual/ import copy import datetime import logging +import re import voluptuous as vol @@ -201,8 +202,12 @@ class ManualAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return '^\\d+$' + return '.+' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 4b08ad67292..c09105c91e0 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -8,6 +8,7 @@ import asyncio import copy import datetime import logging +import re import voluptuous as vol @@ -237,8 +238,12 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return '^\\d+$' + return '.+' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 1422136c405..0298c7384a2 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/alarm_control_panel.mqtt/ """ import asyncio import logging +import re import voluptuous as vol @@ -117,8 +118,12 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): @property def code_format(self): - """One or more characters if code is defined.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return '^\\d+$' + return '.+' @asyncio.coroutine def async_alarm_disarm(self, code=None): diff --git a/homeassistant/components/alarm_control_panel/nx584.py b/homeassistant/components/alarm_control_panel/nx584.py index ceb79c1dc7b..67d3725fc38 100644 --- a/homeassistant/components/alarm_control_panel/nx584.py +++ b/homeassistant/components/alarm_control_panel/nx584.py @@ -69,8 +69,8 @@ class NX584Alarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return che characters if code is defined.""" - return '[0-9]{4}([0-9]{2})?' + """Return one or more digits/characters.""" + return '^\\d+$' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index 3b991c5b236..c08ac3c0ea0 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.simplisafe/ """ import logging +import re import voluptuous as vol @@ -83,8 +84,12 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters if code is defined.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return '^\\d+$' + return '.+' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index 74d63b1fb9c..6651334400f 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -60,8 +60,8 @@ class VerisureAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return the code format as regex.""" - return '^\\d{%s}$' % self._digits + """Return one or more digits/characters.""" + return '^\\d+$' @property def changed_by(self): From 79efb0e607282118cd3318cf536b64c81eb98769 Mon Sep 17 00:00:00 2001 From: Bakkoda Date: Tue, 29 May 2018 01:51:14 -0400 Subject: [PATCH 075/137] Update mfi.py (#14667) Add ability to read door sensor states from the mPort. --- homeassistant/components/sensor/mfi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/mfi.py b/homeassistant/components/sensor/mfi.py index ecea0815e79..f6bec3284c3 100644 --- a/homeassistant/components/sensor/mfi.py +++ b/homeassistant/components/sensor/mfi.py @@ -33,6 +33,7 @@ DIGITS = { SENSOR_MODELS = [ 'Ubiquiti mFi-THS', 'Ubiquiti mFi-CS', + 'Ubiquiti mFi-DS', 'Outlet', 'Input Analog', 'Input Digital', From d36c7c3de792cb5c74717af53c3b8eed71098957 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Mon, 28 May 2018 23:42:27 -0700 Subject: [PATCH 076/137] Increase Eufy's requirement on lakeside (#14671) python-lakeside was broken with at least some versions of the Python protobuf code, so bump the requirement to a fixed version. --- homeassistant/components/eufy.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eufy.py b/homeassistant/components/eufy.py index 892c0b9972a..e86e7348d58 100644 --- a/homeassistant/components/eufy.py +++ b/homeassistant/components/eufy.py @@ -15,7 +15,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['lakeside==0.6'] +REQUIREMENTS = ['lakeside==0.7'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ef4914a5ec0..3e79b2981f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -477,7 +477,7 @@ keyrings.alt==3.1 konnected==0.1.2 # homeassistant.components.eufy -lakeside==0.6 +lakeside==0.7 # homeassistant.components.device_tracker.owntracks # homeassistant.components.device_tracker.owntracks_http From 8c7f0669c699bb927eaa95c53fb9756ba72c33ae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 29 May 2018 02:51:08 -0400 Subject: [PATCH 077/137] Allow hassio frontend development (#14675) * Allow hassio frontend development * Fix tests --- homeassistant/components/hassio/__init__.py | 24 ++++++++++++++++++++- tests/components/hassio/test_init.py | 20 +++++++++++------ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index aa24cc61af3..45c35dcdd2a 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -28,6 +28,15 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'hassio' DEPENDENCIES = ['http'] +CONF_FRONTEND_REPO = 'development_repo' + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN): vol.Schema({ + vol.Optional(CONF_FRONTEND_REPO): cv.isdir, + }), +}, extra=vol.ALLOW_EXTRA) + + DATA_HOMEASSISTANT_VERSION = 'hassio_hass_version' HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) @@ -142,7 +151,13 @@ def async_setup(hass, config): try: host = os.environ['HASSIO'] except KeyError: - _LOGGER.error("No Hass.io supervisor detect") + _LOGGER.error("Missing HASSIO environment variable.") + return False + + try: + os.environ['HASSIO_TOKEN'] + except KeyError: + _LOGGER.error("Missing HASSIO_TOKEN environment variable.") return False websession = hass.helpers.aiohttp_client.async_get_clientsession() @@ -152,6 +167,13 @@ def async_setup(hass, config): _LOGGER.error("Not connected with Hass.io") return False + # This overrides the normal API call that would be forwarded + development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO) + if development_repo is not None: + hass.http.register_static_path( + '/api/hassio/app-es5', + os.path.join(development_repo, 'hassio/build-es5'), False) + hass.http.register_view(HassIOView(host, websession)) if 'frontend' in hass.config.components: diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index e17419e7fd5..f67a6cbccec 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -9,6 +9,12 @@ from homeassistant.components.hassio import async_check_config from tests.common import mock_coro +MOCK_ENVIRON = { + 'HASSIO': '127.0.0.1', + 'HASSIO_TOKEN': 'abcdefgh', +} + + @asyncio.coroutine def test_setup_api_ping(hass, aioclient_mock): """Test setup with API ping.""" @@ -18,7 +24,7 @@ def test_setup_api_ping(hass, aioclient_mock): "http://127.0.0.1/homeassistant/info", json={ 'result': 'ok', 'data': {'last_version': '10.0'}}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', {}) assert result @@ -38,7 +44,7 @@ def test_setup_api_push_api_data(hass, aioclient_mock): aioclient_mock.post( "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': { 'api_password': "123456", @@ -66,7 +72,7 @@ def test_setup_api_push_api_data_server_host(hass, aioclient_mock): aioclient_mock.post( "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': { 'api_password': "123456", @@ -95,7 +101,7 @@ def test_setup_api_push_api_data_default(hass, aioclient_mock): aioclient_mock.post( "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': {}, 'hassio': {} @@ -119,7 +125,7 @@ def test_setup_core_push_timezone(hass, aioclient_mock): aioclient_mock.post( "http://127.0.0.1/supervisor/options", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'hassio': {}, 'homeassistant': { @@ -143,7 +149,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock): aioclient_mock.get( "http://127.0.0.1/homeassistant/info", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \ + with patch.dict(os.environ, MOCK_ENVIRON), \ patch.dict(os.environ, {'HASSIO_TOKEN': "123456"}): result = yield from async_setup_component(hass, 'hassio', { 'hassio': {}, @@ -165,7 +171,7 @@ def test_fail_setup_without_environ_var(hass): @asyncio.coroutine def test_fail_setup_cannot_connect(hass): """Fail setup if cannot connect.""" - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \ + with patch.dict(os.environ, MOCK_ENVIRON), \ patch('homeassistant.components.hassio.HassIO.is_connected', Mock(return_value=mock_coro(None))): result = yield from async_setup_component(hass, 'hassio', {}) From f2a2f2cca5f1a677a57a4ae0bfb87e396e143237 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 29 May 2018 10:15:30 +0200 Subject: [PATCH 078/137] Ignore unsupported Sonos favorite lists (#14665) --- .../components/media_player/sonos.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 06e5f3befe4..0f536e1edfb 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -427,15 +427,18 @@ class SonosDevice(MediaPlayerDevice): self.update_volume() self._favorites = [] - for fav in self.soco.music_library.get_sonos_favorites(): - # SoCo 0.14 raises a generic Exception on invalid xml in favorites. - # Filter those out now so our list is safe to use. - try: - if fav.reference.get_uri(): - self._favorites.append(fav) - # pylint: disable=broad-except - except Exception: - _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) + # SoCo 0.14 raises a generic Exception on invalid xml in favorites. + # Filter those out now so our list is safe to use. + # pylint: disable=broad-except + try: + for fav in self.soco.music_library.get_sonos_favorites(): + try: + if fav.reference.get_uri(): + self._favorites.append(fav) + except Exception: + _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) + except Exception: + _LOGGER.debug("Ignoring invalid favorite list") def _radio_artwork(self, url): """Return the private URL with artwork for a radio stream.""" From fcb60d472ea0fcc0121c8163be15d1987be775e9 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 29 May 2018 15:03:45 +0200 Subject: [PATCH 079/137] MQTT Cover Fix Assumed State (#14672) --- homeassistant/components/cover/mqtt.py | 5 +++++ tests/components/cover/test_mqtt.py | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 0f31d3a9fe0..235ff5799cc 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -235,6 +235,11 @@ class MqttCover(MqttAvailability, CoverDevice): """No polling needed.""" return False + @property + def assumed_state(self): + """Return true if we do optimistic updates.""" + return self._optimistic + @property def name(self): """Return the name of the cover.""" diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index 23a7b32fc28..aea6398e3ae 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -2,8 +2,8 @@ import unittest from homeassistant.setup import setup_component -from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN,\ - STATE_UNAVAILABLE +from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN, \ + STATE_UNAVAILABLE, ATTR_ASSUMED_STATE import homeassistant.components.cover as cover from homeassistant.components.cover.mqtt import MqttCover @@ -40,6 +40,7 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNKNOWN, state.state) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) fire_mqtt_message(self.hass, 'state-topic', '0') self.hass.block_till_done() @@ -112,6 +113,7 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNKNOWN, state.state) + self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) cover.open_cover(self.hass, 'cover.test') self.hass.block_till_done() From eff1d1f14e6335eb3194a970279085bc38cc8695 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 29 May 2018 09:05:07 -0400 Subject: [PATCH 080/137] zha: fix temperature rounding for ZHA temperature sensors. (#14669) --- homeassistant/components/sensor/zha.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 3051d8f2afa..53e0e8d0329 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -114,9 +114,11 @@ class TemperatureSensor(Sensor): """Return the state of the entity.""" if self._state is None: return None - celsius = round(float(self._state) / 100, 1) - return convert_temperature( - celsius, TEMP_CELSIUS, self.unit_of_measurement) + celsius = self._state / 100 + return round(convert_temperature(celsius, + TEMP_CELSIUS, + self.unit_of_measurement), + 1) class RelativeHumiditySensor(Sensor): From 3b38de63ea92296b49eae196c8802837dae6f03d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 29 May 2018 16:03:00 +0200 Subject: [PATCH 081/137] Allow user-defined sensors (#14613) * Allow user-defined sensors * Require element for resources * Don't use .get() --- homeassistant/components/sensor/netdata.py | 164 ++++++++++----------- requirements_all.txt | 3 + 2 files changed, 84 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/sensor/netdata.py b/homeassistant/components/sensor/netdata.py index 0d2a542c7bb..2d08159967c 100644 --- a/homeassistant/components/sensor/netdata.py +++ b/homeassistant/components/sensor/netdata.py @@ -4,154 +4,152 @@ Support gathering system information of hosts which are running netdata. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.netdata/ """ -import logging from datetime import timedelta -from urllib.parse import urlsplit +import logging -import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_PORT, CONF_NAME, CONF_RESOURCES) + CONF_HOST, CONF_ICON, CONF_NAME, CONF_PORT, CONF_RESOURCES) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['netdata==0.1.2'] _LOGGER = logging.getLogger(__name__) -_RESOURCE = 'api/v1' -_REALTIME = 'before=0&after=-1&options=seconds' + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) + +CONF_ELEMENT = 'element' DEFAULT_HOST = 'localhost' DEFAULT_NAME = 'Netdata' -DEFAULT_PORT = '19999' +DEFAULT_PORT = 19999 -SCAN_INTERVAL = timedelta(minutes=1) +DEFAULT_ICON = 'mdi:desktop-classic' -SENSOR_TYPES = { - 'memory_free': ['RAM Free', 'MiB', 'system.ram', 'free', 1], - 'memory_used': ['RAM Used', 'MiB', 'system.ram', 'used', 1], - 'memory_cached': ['RAM Cached', 'MiB', 'system.ram', 'cached', 1], - 'memory_buffers': ['RAM Buffers', 'MiB', 'system.ram', 'buffers', 1], - 'swap_free': ['Swap Free', 'MiB', 'system.swap', 'free', 1], - 'swap_used': ['Swap Used', 'MiB', 'system.swap', 'used', 1], - 'processes_running': ['Processes Running', 'Count', 'system.processes', - 'running', 0], - 'processes_blocked': ['Processes Blocked', 'Count', 'system.processes', - 'blocked', 0], - 'system_load': ['System Load', '15 min', 'system.load', 'load15', 2], - 'system_io_in': ['System IO In', 'Count', 'system.io', 'in', 0], - 'system_io_out': ['System IO Out', 'Count', 'system.io', 'out', 0], - 'ipv4_in': ['IPv4 In', 'kb/s', 'system.ipv4', 'received', 0], - 'ipv4_out': ['IPv4 Out', 'kb/s', 'system.ipv4', 'sent', 0], - 'disk_free': ['Disk Free', 'GiB', 'disk_space._', 'avail', 2], - 'cpu_iowait': ['CPU IOWait', '%', 'system.cpu', 'iowait', 1], - 'cpu_user': ['CPU User', '%', 'system.cpu', 'user', 1], - 'cpu_system': ['CPU System', '%', 'system.cpu', 'system', 1], - 'cpu_softirq': ['CPU SoftIRQ', '%', 'system.cpu', 'softirq', 1], - 'cpu_guest': ['CPU Guest', '%', 'system.cpu', 'guest', 1], - 'uptime': ['Uptime', 's', 'system.uptime', 'uptime', 0], - 'packets_received': ['Packets Received', 'packets/s', 'ipv4.packets', - 'received', 0], - 'packets_sent': ['Packets Sent', 'packets/s', 'ipv4.packets', - 'sent', 0], - 'connections': ['Active Connections', 'Count', - 'netfilter.conntrack_sockets', 'connections', 0] -} +RESOURCE_SCHEMA = vol.Any({ + vol.Required(CONF_ELEMENT): cv.string, + vol.Optional(CONF_ICON, default=DEFAULT_ICON): cv.icon, + vol.Optional(CONF_NAME): cv.string, +}) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_RESOURCES, default=['memory_free']): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Required(CONF_RESOURCES): vol.Schema({cv.string: RESOURCE_SCHEMA}), }) -# pylint: disable=unused-variable -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 Netdata sensor.""" + from netdata import Netdata + name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = 'http://{}:{}'.format(host, port) - data_url = '{}/{}/data?chart='.format(url, _RESOURCE) resources = config.get(CONF_RESOURCES) - values = {} - for key, value in sorted(SENSOR_TYPES.items()): - if key in resources: - values.setdefault(value[2], []).append(key) + session = async_get_clientsession(hass) + netdata = NetdataData(Netdata(host, hass.loop, session, port=port)) + await netdata.async_update() + + if netdata.api.metrics is None: + raise PlatformNotReady dev = [] - for chart in values: - rest_url = '{}{}&{}'.format(data_url, chart, _REALTIME) - rest = NetdataData(rest_url) - rest.update() - for sensor_type in values[chart]: - dev.append(NetdataSensor(rest, name, sensor_type)) + for entry, data in resources.items(): + sensor = entry + element = data[CONF_ELEMENT] + sensor_name = icon = None + try: + resource_data = netdata.api.metrics[sensor] + unit = '%' if resource_data['units'] == 'percentage' else \ + resource_data['units'] + if data is not None: + sensor_name = data.get(CONF_NAME) + icon = data.get(CONF_ICON) + except KeyError: + _LOGGER.error("Sensor is not available: %s", sensor) + continue - add_devices(dev, True) + dev.append(NetdataSensor( + netdata, name, sensor, sensor_name, element, icon, unit)) + + async_add_devices(dev, True) class NetdataSensor(Entity): """Implementation of a Netdata sensor.""" - def __init__(self, rest, name, sensor_type): + def __init__( + self, netdata, name, sensor, sensor_name, element, icon, unit): """Initialize the Netdata sensor.""" - self.rest = rest - self.type = sensor_type - self._name = '{} {}'.format(name, SENSOR_TYPES[self.type][0]) - self._precision = SENSOR_TYPES[self.type][4] - self._unit_of_measurement = SENSOR_TYPES[self.type][1] + self.netdata = netdata + self._state = None + self._sensor = sensor + self._element = element + self._sensor_name = self._sensor if sensor_name is None else \ + sensor_name + self._name = name + self._icon = icon + self._unit_of_measurement = unit @property def name(self): """Return the name of the sensor.""" - return self._name + return '{} {}'.format(self._name, self._sensor_name) @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._icon + @property def state(self): """Return the state of the resources.""" - value = self.rest.data - - if value is not None: - netdata_id = SENSOR_TYPES[self.type][3] - if netdata_id in value: - return "{0:.{1}f}".format(value[netdata_id], self._precision) - return None + return self._state @property def available(self): """Could the resource be accessed during the last update call.""" - return self.rest.available + return self.netdata.available - def update(self): + async def async_update(self): """Get the latest data from Netdata REST API.""" - self.rest.update() + await self.netdata.async_update() + resource_data = self.netdata.api.metrics.get(self._sensor) + self._state = round( + resource_data['dimensions'][self._element]['value'], 2) class NetdataData(object): """The class for handling the data retrieval.""" - def __init__(self, resource): + def __init__(self, api): """Initialize the data object.""" - self._resource = resource - self.data = None + self.api = api self.available = True - def update(self): + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): """Get the latest data from the Netdata REST API.""" + from netdata.exceptions import NetdataError + try: - response = requests.get(self._resource, timeout=5) - det = response.json() - self.data = {k: v for k, v in zip(det['labels'], det['data'][0])} + await self.api.get_allmetrics() self.available = True - except requests.exceptions.ConnectionError: - _LOGGER.error("Connection error: %s", urlsplit(self._resource)[1]) - self.data = None + except NetdataError: + _LOGGER.error("Unable to retrieve data from Netdata") self.available = False diff --git a/requirements_all.txt b/requirements_all.txt index 3e79b2981f5..3e7b966af4f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -567,6 +567,9 @@ nad_receiver==0.0.9 # homeassistant.components.light.nanoleaf_aurora nanoleaf==0.4.1 +# homeassistant.components.sensor.netdata +netdata==0.1.2 + # homeassistant.components.discovery netdisco==1.4.1 From 8c93b484c449f653191b616beb52ec77dd878251 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 29 May 2018 16:09:53 +0200 Subject: [PATCH 082/137] deCONZ - Option to load or not to load clip sensors on start (#14480) * Option to load or not to load clip sensors on start * Full flow * Fix config flow and add tests * Fix attribute dark reporting properly * Imported and properly configured deCONZ shouldn't need extra input to create config entry --- .../components/binary_sensor/deconz.py | 10 ++- .../components/deconz/.translations/en.json | 8 ++- homeassistant/components/deconz/__init__.py | 8 ++- .../components/deconz/config_flow.py | 69 +++++++++++++------ homeassistant/components/deconz/const.py | 2 + homeassistant/components/deconz/strings.json | 8 ++- homeassistant/components/sensor/deconz.py | 11 ++- tests/components/binary_sensor/test_deconz.py | 18 ++++- tests/components/deconz/test_config_flow.py | 37 ++++++++-- tests/components/deconz/test_init.py | 18 +++++ tests/components/sensor/test_deconz.py | 18 ++++- 11 files changed, 165 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 9faa703d13c..6f59da0755a 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -6,7 +6,8 @@ https://home-assistant.io/components/binary_sensor.deconz/ """ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) + CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, + DATA_DECONZ_UNSUB) from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -27,10 +28,13 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """Add binary sensor from deCONZ.""" from pydeconz.sensor import DECONZ_BINARY_SENSOR entities = [] + allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) for sensor in sensors: - if sensor.type in DECONZ_BINARY_SENSOR: + if sensor.type in DECONZ_BINARY_SENSOR and \ + not (not allow_clip_sensor and sensor.type.startswith('CLIP')): entities.append(DeconzBinarySensor(sensor)) async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) @@ -103,6 +107,6 @@ class DeconzBinarySensor(BinarySensorDevice): attr = {} if self._sensor.battery: attr[ATTR_BATTERY_LEVEL] = self._sensor.battery - if self._sensor.type in PRESENCE and self._sensor.dark: + if self._sensor.type in PRESENCE and self._sensor.dark is not None: attr['dark'] = self._sensor.dark return attr diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 0009986d45f..a2f90e49e3a 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -19,8 +19,14 @@ "link": { "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button", "title": "Link with deCONZ" + }, + "options": { + "title": "Extra configuration options for deCONZ", + "data": { + "allow_clip_sensor": "Allow importing virtual sensors" + } } }, - "title": "deCONZ" + "title": "deCONZ Zigbee gateway" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index bbab4029d7e..850645225d0 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -19,8 +19,8 @@ from homeassistant.util.json import load_json # Loading the config flow file will register the flow from .config_flow import configured_hosts from .const import ( - CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, - DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) + CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, + DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) REQUIREMENTS = ['pydeconz==38'] @@ -104,8 +104,10 @@ async def async_setup_entry(hass, config_entry): def async_add_remote(sensors): """Setup remote from deCONZ.""" from pydeconz.sensor import SWITCH as DECONZ_REMOTE + allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) for sensor in sensors: - if sensor.type in DECONZ_REMOTE: + if sensor.type in DECONZ_REMOTE and \ + not (not allow_clip_sensor and sensor.type.startswith('CLIP')): hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor)) hass.data[DATA_DECONZ_UNSUB].append( async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote)) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index e900782ea65..cb7c3aad7fd 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -8,13 +8,15 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers import aiohttp_client from homeassistant.util.json import load_json -from .const import CONFIG_FILE, DOMAIN +from .const import CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN + +CONF_BRIDGEID = 'bridgeid' @callback def configured_hosts(hass): """Return a set of the configured hosts.""" - return set(entry.data['host'] for entry + return set(entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN)) @@ -30,7 +32,12 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): self.deconz_config = {} async def async_step_init(self, user_input=None): - """Handle a deCONZ config flow start.""" + """Handle a deCONZ config flow start. + + Only allows one instance to be set up. + If only one bridge is found go to link step. + If more than one bridge is found let user choose bridge to link. + """ from pydeconz.utils import async_discovery if configured_hosts(self.hass): @@ -65,7 +72,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): async def async_step_link(self, user_input=None): """Attempt to link with the deCONZ bridge.""" - from pydeconz.utils import async_get_api_key, async_get_bridgeid + from pydeconz.utils import async_get_api_key errors = {} if user_input is not None: @@ -75,13 +82,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): api_key = await async_get_api_key(session, **self.deconz_config) if api_key: self.deconz_config[CONF_API_KEY] = api_key - if 'bridgeid' not in self.deconz_config: - self.deconz_config['bridgeid'] = await async_get_bridgeid( - session, **self.deconz_config) - return self.async_create_entry( - title='deCONZ-' + self.deconz_config['bridgeid'], - data=self.deconz_config - ) + return await self.async_step_options() errors['base'] = 'no_key' return self.async_show_form( @@ -89,6 +90,34 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): errors=errors, ) + async def async_step_options(self, user_input=None): + """Extra options for deCONZ. + + CONF_CLIP_SENSOR -- Allow user to choose if they want clip sensors. + """ + from pydeconz.utils import async_get_bridgeid + + if user_input is not None: + self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = \ + user_input[CONF_ALLOW_CLIP_SENSOR] + + if CONF_BRIDGEID not in self.deconz_config: + session = aiohttp_client.async_get_clientsession(self.hass) + self.deconz_config[CONF_BRIDGEID] = await async_get_bridgeid( + session, **self.deconz_config) + + return self.async_create_entry( + title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], + data=self.deconz_config + ) + + return self.async_show_form( + step_id='options', + data_schema=vol.Schema({ + vol.Optional(CONF_ALLOW_CLIP_SENSOR): bool, + }), + ) + async def async_step_discovery(self, discovery_info): """Prepare configuration for a discovered deCONZ bridge. @@ -97,7 +126,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): deconz_config = {} deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST) deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT) - deconz_config['bridgeid'] = discovery_info.get('serial') + deconz_config[CONF_BRIDGEID] = discovery_info.get('serial') config_file = await self.hass.async_add_job( load_json, self.hass.config.path(CONFIG_FILE)) @@ -121,19 +150,15 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): Otherwise we will delegate to `link` step which will ask user to link the bridge. """ - from pydeconz.utils import async_get_bridgeid - if configured_hosts(self.hass): return self.async_abort(reason='one_instance_only') - elif CONF_API_KEY not in import_config: - self.deconz_config = import_config + + self.deconz_config = import_config + if CONF_API_KEY not in import_config: return await self.async_step_link() - if 'bridgeid' not in import_config: - session = aiohttp_client.async_get_clientsession(self.hass) - import_config['bridgeid'] = await async_get_bridgeid( - session, **import_config) + self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True return self.async_create_entry( - title='deCONZ-' + import_config['bridgeid'], - data=import_config + title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], + data=self.deconz_config ) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 48e5ea75d68..43f3c6441da 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -8,3 +8,5 @@ CONFIG_FILE = 'deconz.conf' DATA_DECONZ_EVENT = 'deconz_events' DATA_DECONZ_ID = 'deconz_entities' DATA_DECONZ_UNSUB = 'deconz_dispatchers' + +CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 7ea68af01c1..cabe58694d2 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -1,6 +1,6 @@ { "config": { - "title": "deCONZ", + "title": "deCONZ Zigbee gateway", "step": { "init": { "title": "Define deCONZ gateway", @@ -12,6 +12,12 @@ "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" + }, + "options": { + "title": "Extra configuration options for deCONZ", + "data":{ + "allow_clip_sensor": "Allow importing virtual sensors" + } } }, "error": { diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index 221cdf2129e..0db06622ad8 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -5,7 +5,8 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) + CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, + DATA_DECONZ_UNSUB) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback @@ -33,14 +34,17 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """Add sensors from deCONZ.""" from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE entities = [] + allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) for sensor in sensors: - if sensor.type in DECONZ_SENSOR: + if sensor.type in DECONZ_SENSOR and \ + not (not allow_clip_sensor and sensor.type.startswith('CLIP')): if sensor.type in DECONZ_REMOTE: if sensor.battery: entities.append(DeconzBattery(sensor)) else: entities.append(DeconzSensor(sensor)) async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) @@ -114,9 +118,12 @@ class DeconzSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" + from pydeconz.sensor import LIGHTLEVEL attr = {} if self._sensor.battery: attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self._sensor.type in LIGHTLEVEL and self._sensor.dark is not None: + attr['dark'] = self._sensor.dark if self.unit_of_measurement == 'Watts': attr[ATTR_CURRENT] = self._sensor.current attr[ATTR_VOLTAGE] = self._sensor.voltage diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/binary_sensor/test_deconz.py index 88dd0dae737..2e33e28fa57 100644 --- a/tests/components/binary_sensor/test_deconz.py +++ b/tests/components/binary_sensor/test_deconz.py @@ -26,7 +26,7 @@ SENSOR = { } -async def setup_bridge(hass, data): +async def setup_bridge(hass, data, allow_clip_sensor=True): """Load the deCONZ binary sensor platform.""" from pydeconz import DeconzSession loop = Mock() @@ -41,7 +41,8 @@ async def setup_bridge(hass, data): hass.data[deconz.DATA_DECONZ_UNSUB] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( - 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + 1, deconz.DOMAIN, 'Mock Title', + {'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test') await hass.config_entries.async_forward_entry_setup( config_entry, 'binary_sensor') # To flush out the service call to update the group @@ -77,3 +78,16 @@ async def test_add_new_sensor(hass): async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) await hass.async_block_till_done() assert "binary_sensor.name" in hass.data[deconz.DATA_DECONZ_ID] + + +async def test_do_not_allow_clip_sensor(hass): + """Test that clip sensors can be ignored.""" + data = {} + await setup_bridge(hass, data, allow_clip_sensor=False) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'CLIPPresence' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index d86475b35ef..df3310f3d6f 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -21,7 +21,9 @@ async def test_flow_works(hass, aioclient_mock): flow = config_flow.DeconzFlowHandler() flow.hass = hass await flow.async_step_init() - result = await flow.async_step_link(user_input={}) + await flow.async_step_link(user_input={}) + result = await flow.async_step_options( + user_input={'allow_clip_sensor': True}) assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' @@ -29,7 +31,8 @@ async def test_flow_works(hass, aioclient_mock): 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF' + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': True } @@ -146,14 +149,14 @@ async def test_bridge_discovery_config_file(hass): 'port': 80, 'serial': 'id' }) - assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' assert result['data'] == { 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF' + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': True } @@ -214,12 +217,34 @@ async def test_import_with_api_key(hass): 'port': 80, 'api_key': '1234567890ABCDEF' }) - assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' assert result['data'] == { 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF' + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': True + } + + +async def test_options(hass, aioclient_mock): + """Test that options work and that bridgeid can be requested.""" + aioclient_mock.get('http://1.2.3.4:80/api/1234567890ABCDEF/config', + json={"bridgeid": "id"}) + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + flow.deconz_config = {'host': '1.2.3.4', + 'port': 80, + 'api_key': '1234567890ABCDEF'} + result = await flow.async_step_options( + user_input={'allow_clip_sensor': False}) + assert result['type'] == 'create_entry' + assert result['title'] == 'deCONZ-id' + assert result['data'] == { + 'bridgeid': 'id', + 'host': '1.2.3.4', + 'port': 80, + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': False } diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 888094deea6..1cee08feb0a 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -172,3 +172,21 @@ async def test_add_new_remote(hass): async_dispatcher_send(hass, 'deconz_new_sensor', [remote]) await hass.async_block_till_done() assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 1 + + +async def test_do_not_allow_clip_sensor(hass): + """Test that clip sensors can be ignored.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, + 'api_key': '1234567890ABCDEF', 'allow_clip_sensor': False} + remote = Mock() + remote.name = 'name' + remote.type = 'CLIPSwitch' + remote.register_async_callback = Mock() + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + + async_dispatcher_send(hass, 'deconz_new_sensor', [remote]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 0 diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py index 8f6a53e6e65..d7cdb458646 100644 --- a/tests/components/sensor/test_deconz.py +++ b/tests/components/sensor/test_deconz.py @@ -41,7 +41,7 @@ SENSOR = { } -async def setup_bridge(hass, data): +async def setup_bridge(hass, data, allow_clip_sensor=True): """Load the deCONZ sensor platform.""" from pydeconz import DeconzSession loop = Mock() @@ -57,7 +57,8 @@ async def setup_bridge(hass, data): hass.data[deconz.DATA_DECONZ_EVENT] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( - 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + 1, deconz.DOMAIN, 'Mock Title', + {'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test') await hass.config_entries.async_forward_entry_setup(config_entry, 'sensor') # To flush out the service call to update the group await hass.async_block_till_done() @@ -97,3 +98,16 @@ async def test_add_new_sensor(hass): async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) await hass.async_block_till_done() assert "sensor.name" in hass.data[deconz.DATA_DECONZ_ID] + + +async def test_do_not_allow_clipsensor(hass): + """Test that clip sensors can be ignored.""" + data = {} + await setup_bridge(hass, data, allow_clip_sensor=False) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'CLIPTemperature' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 From 41054296396def7d004011491e4671350289c079 Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Tue, 29 May 2018 10:23:12 -0400 Subject: [PATCH 083/137] Add asyncio support for Ebox (#14183) * Fix Ebox sensor * Fix #14183 comments * Update ebox.py * Update ebox.py * Continue fixing comments --- homeassistant/components/sensor/ebox.py | 45 +++++++++++++++---------- requirements_all.txt | 3 ++ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/sensor/ebox.py b/homeassistant/components/sensor/ebox.py index aca2d7bdb9a..d7b867081a3 100644 --- a/homeassistant/components/sensor/ebox.py +++ b/homeassistant/components/sensor/ebox.py @@ -9,7 +9,6 @@ https://home-assistant.io/components/sensor.ebox/ import logging from datetime import timedelta -import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -18,9 +17,11 @@ from homeassistant.const import ( CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_MONITORED_VARIABLES) from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +from homeassistant.exceptions import PlatformNotReady -# pylint: disable=import-error -REQUIREMENTS = [] # ['pyebox==0.1.0'] - disabled because it breaks pip10 + +REQUIREMENTS = ['pyebox==1.1.4'] _LOGGER = logging.getLogger(__name__) @@ -32,7 +33,8 @@ PERCENT = '%' # type: str DEFAULT_NAME = 'EBox' REQUESTS_TIMEOUT = 15 -SCAN_INTERVAL = timedelta(minutes=5) +SCAN_INTERVAL = timedelta(minutes=15) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) SENSOR_TYPES = { 'usage': ['Usage', PERCENT, 'mdi:percent'], @@ -62,25 +64,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -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 EBox sensor.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - try: - ebox_data = EBoxData(username, password) - ebox_data.update() - except requests.exceptions.HTTPError as error: - _LOGGER.error("Failed login: %s", error) - return False + httpsession = hass.helpers.aiohttp_client.async_get_clientsession() + ebox_data = EBoxData(username, password, httpsession) name = config.get(CONF_NAME) + from pyebox.client import PyEboxError + try: + await ebox_data.async_update() + except PyEboxError as exp: + _LOGGER.error("Failed login: %s", exp) + raise PlatformNotReady + sensors = [] for variable in config[CONF_MONITORED_VARIABLES]: sensors.append(EBoxSensor(ebox_data, variable, name)) - add_devices(sensors, True) + async_add_devices(sensors, True) class EBoxSensor(Entity): @@ -116,9 +122,9 @@ class EBoxSensor(Entity): """Icon to use in the frontend, if any.""" return self._icon - def update(self): + async def async_update(self): """Get the latest data from EBox and update the state.""" - self.ebox_data.update() + await self.ebox_data.async_update() if self.type in self.ebox_data.data: self._state = round(self.ebox_data.data[self.type], 2) @@ -126,18 +132,21 @@ class EBoxSensor(Entity): class EBoxData(object): """Get data from Ebox.""" - def __init__(self, username, password): + def __init__(self, username, password, httpsession): """Initialize the data object.""" from pyebox import EboxClient - self.client = EboxClient(username, password, REQUESTS_TIMEOUT) + self.client = EboxClient(username, password, + REQUESTS_TIMEOUT, httpsession) self.data = {} - def update(self): + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): """Get the latest data from Ebox.""" from pyebox.client import PyEboxError try: - self.client.fetch_data() + await self.client.fetch_data() except PyEboxError as exp: _LOGGER.error("Error on receive last EBox data: %s", exp) return + # Update data self.data = self.client.get_data() diff --git a/requirements_all.txt b/requirements_all.txt index 3e7b966af4f..958b0f1027e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -774,6 +774,9 @@ pydispatcher==2.0.5 # homeassistant.components.android_ip_webcam pydroid-ipcam==0.8 +# homeassistant.components.sensor.ebox +pyebox==1.1.4 + # homeassistant.components.climate.econet pyeconet==0.0.5 From 084b3287ab6755b70cd812a3c8fd83a8098159b5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 29 May 2018 13:02:16 -0600 Subject: [PATCH 084/137] Add sensors and services to RainMachine (#14326) * Starting to add attributes * All attributes added to programs * Basic zone attributes in place * Added advanced properties for zones * We shouldn't calculate the MAC with every entity * Small fixes * Basic framework for push in play * I THINK IT'S WORKING * Some state cleanup * Restart * Restart part 2 * Added stub for service schema * Update * Added services * Small service description update * Lint * Updated CODEOWNERS * Moving to async methods * Fixed coverage test * Lint * Removed unnecessary hass reference * Lint * Lint * Round 1 of Owner-requested changes * Round 2 of Owner-requested changes * Round 3 of Owner-requested changes * Round 4 (final for now) of Owner-requested changes * Hound * Updated package requirements * Lint * Collaborator-requested changes * Collaborator-requested changes * More small tweaks * One more small tweak * Bumping Travis and Coveralls --- .coveragerc | 2 +- CODEOWNERS | 3 +- .../components/binary_sensor/rainmachine.py | 102 ++++++++ homeassistant/components/rainmachine.py | 132 ---------- .../components/rainmachine/__init__.py | 226 ++++++++++++++++++ .../components/rainmachine/services.yaml | 32 +++ .../components/sensor/rainmachine.py | 88 +++++++ .../components/switch/rainmachine.py | 79 +++--- requirements_all.txt | 2 +- 9 files changed, 490 insertions(+), 176 deletions(-) create mode 100644 homeassistant/components/binary_sensor/rainmachine.py delete mode 100644 homeassistant/components/rainmachine.py create mode 100644 homeassistant/components/rainmachine/__init__.py create mode 100644 homeassistant/components/rainmachine/services.yaml create mode 100644 homeassistant/components/sensor/rainmachine.py diff --git a/.coveragerc b/.coveragerc index 3d1bbab8456..8d884dc53e6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -219,7 +219,7 @@ omit = homeassistant/components/raincloud.py homeassistant/components/*/raincloud.py - homeassistant/components/rainmachine.py + homeassistant/components/rainmachine/* homeassistant/components/*/rainmachine.py homeassistant/components/raspihats.py diff --git a/CODEOWNERS b/CODEOWNERS index 32639fed43c..0da8353e5aa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -78,7 +78,6 @@ homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/upnp.py @dgomes homeassistant/components/sensor/waqi.py @andrey-git -homeassistant/components/switch/rainmachine.py @bachya homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi @@ -100,6 +99,8 @@ homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf homeassistant/components/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza +homeassistant/components/rainmachine/* @bachya +homeassistant/components/*/rainmachine.py @bachya homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei diff --git a/homeassistant/components/binary_sensor/rainmachine.py b/homeassistant/components/binary_sensor/rainmachine.py new file mode 100644 index 00000000000..601a73298af --- /dev/null +++ b/homeassistant/components/binary_sensor/rainmachine.py @@ -0,0 +1,102 @@ +""" +This platform provides binary sensors for key RainMachine data. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.rainmachine/ +""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.rainmachine import ( + BINARY_SENSORS, DATA_RAINMACHINE, DATA_UPDATE_TOPIC, TYPE_FREEZE, + TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH, + TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity) +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['rainmachine'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the RainMachine Switch platform.""" + if discovery_info is None: + return + + rainmachine = hass.data[DATA_RAINMACHINE] + + binary_sensors = [] + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + name, icon = BINARY_SENSORS[sensor_type] + binary_sensors.append( + RainMachineBinarySensor(rainmachine, sensor_type, name, icon)) + + add_devices(binary_sensors, True) + + +class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): + """A sensor implementation for raincloud device.""" + + def __init__(self, rainmachine, sensor_type, name, icon): + """Initialize the sensor.""" + super().__init__(rainmachine) + + self._icon = icon + self._name = name + self._sensor_type = sensor_type + self._state = None + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}'.format( + self.rainmachine.device_mac.replace(':', ''), self._sensor_type) + + @callback + def update_data(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC, + self.update_data) + + def update(self): + """Update the state.""" + if self._sensor_type == TYPE_FREEZE: + self._state = self.rainmachine.restrictions['current']['freeze'] + elif self._sensor_type == TYPE_FREEZE_PROTECTION: + self._state = self.rainmachine.restrictions['global'][ + 'freezeProtectEnabled'] + elif self._sensor_type == TYPE_HOT_DAYS: + self._state = self.rainmachine.restrictions['global'][ + 'hotDaysExtraWatering'] + elif self._sensor_type == TYPE_HOURLY: + self._state = self.rainmachine.restrictions['current']['hourly'] + elif self._sensor_type == TYPE_MONTH: + self._state = self.rainmachine.restrictions['current']['month'] + elif self._sensor_type == TYPE_RAINDELAY: + self._state = self.rainmachine.restrictions['current']['rainDelay'] + elif self._sensor_type == TYPE_RAINSENSOR: + self._state = self.rainmachine.restrictions['current'][ + 'rainSensor'] + elif self._sensor_type == TYPE_WEEKDAY: + self._state = self.rainmachine.restrictions['current']['weekDay'] diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py deleted file mode 100644 index f2d5893d60b..00000000000 --- a/homeassistant/components/rainmachine.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -This component provides support for RainMachine sprinkler controllers. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/rainmachine/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, - CONF_SWITCHES) -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['regenmaschine==0.4.1'] - -_LOGGER = logging.getLogger(__name__) - -DATA_RAINMACHINE = 'data_rainmachine' -DOMAIN = 'rainmachine' - -NOTIFICATION_ID = 'rainmachine_notification' -NOTIFICATION_TITLE = 'RainMachine Component Setup' - -CONF_ZONE_RUN_TIME = 'zone_run_time' - -DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' -DEFAULT_ICON = 'mdi:water' -DEFAULT_PORT = 8080 -DEFAULT_SSL = True - -PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) - -SWITCH_SCHEMA = vol.Schema({ - vol.Optional(CONF_ZONE_RUN_TIME): - cv.positive_int -}) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema({ - vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_SWITCHES): SWITCH_SCHEMA, - }) - }, - extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the RainMachine component.""" - from regenmaschine import Authenticator, Client - from regenmaschine.exceptions import HTTPError - from requests.exceptions import ConnectTimeout - - conf = config[DOMAIN] - ip_address = conf[CONF_IP_ADDRESS] - password = conf[CONF_PASSWORD] - port = conf[CONF_PORT] - ssl = conf[CONF_SSL] - - _LOGGER.debug('Setting up RainMachine client') - - try: - auth = Authenticator.create_local( - ip_address, password, port=port, https=ssl) - client = Client(auth) - hass.data[DATA_RAINMACHINE] = RainMachine(client) - except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info: - _LOGGER.error('An error occurred: %s', str(exc_info)) - hass.components.persistent_notification.create( - 'Error: {0}
' - 'You will need to restart hass after fixing.' - ''.format(exc_info), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False - - _LOGGER.debug('Setting up switch platform') - switch_config = conf.get(CONF_SWITCHES, {}) - discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config) - - _LOGGER.debug('Setup complete') - - return True - - -class RainMachine(object): - """Define a generic RainMachine object.""" - - def __init__(self, client): - """Initialize.""" - self.client = client - self.device_mac = self.client.provision.wifi()['macAddress'] - - -class RainMachineEntity(Entity): - """Define a generic RainMachine entity.""" - - def __init__(self, - rainmachine, - rainmachine_type, - rainmachine_entity_id, - icon=DEFAULT_ICON): - """Initialize.""" - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._icon = icon - self._rainmachine_type = rainmachine_type - self._rainmachine_entity_id = rainmachine_entity_id - self.rainmachine = rainmachine - - @property - def device_state_attributes(self) -> dict: - """Return the state attributes.""" - return self._attrs - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_{1}_{2}'.format( - self.rainmachine.device_mac.replace( - ':', ''), self._rainmachine_type, - self._rainmachine_entity_id) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py new file mode 100644 index 00000000000..7ee6b063720 --- /dev/null +++ b/homeassistant/components/rainmachine/__init__.py @@ -0,0 +1,226 @@ +""" +Support for RainMachine devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/rainmachine/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, CONF_PASSWORD, + CONF_PORT, CONF_SENSORS, CONF_SSL, CONF_MONITORED_CONDITIONS, + CONF_SWITCHES) +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['regenmaschine==0.4.2'] + +_LOGGER = logging.getLogger(__name__) + +DATA_RAINMACHINE = 'data_rainmachine' +DOMAIN = 'rainmachine' + +NOTIFICATION_ID = 'rainmachine_notification' +NOTIFICATION_TITLE = 'RainMachine Component Setup' + +DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) +PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) + +CONF_PROGRAM_ID = 'program_id' +CONF_ZONE_ID = 'zone_id' +CONF_ZONE_RUN_TIME = 'zone_run_time' + +DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' +DEFAULT_ICON = 'mdi:water' +DEFAULT_PORT = 8080 +DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) +DEFAULT_SSL = True +DEFAULT_ZONE_RUN = 60 * 10 + +TYPE_FREEZE = 'freeze' +TYPE_FREEZE_PROTECTION = 'freeze_protection' +TYPE_FREEZE_TEMP = 'freeze_protect_temp' +TYPE_HOT_DAYS = 'extra_water_on_hot_days' +TYPE_HOURLY = 'hourly' +TYPE_MONTH = 'month' +TYPE_RAINDELAY = 'raindelay' +TYPE_RAINSENSOR = 'rainsensor' +TYPE_WEEKDAY = 'weekday' + +BINARY_SENSORS = { + TYPE_FREEZE: ('Freeze Restrictions', 'mdi:cancel'), + TYPE_FREEZE_PROTECTION: ('Freeze Protection', 'mdi:weather-snowy'), + TYPE_HOT_DAYS: ('Extra Water on Hot Days', 'mdi:thermometer-lines'), + TYPE_HOURLY: ('Hourly Restrictions', 'mdi:cancel'), + TYPE_MONTH: ('Month Restrictions', 'mdi:cancel'), + TYPE_RAINDELAY: ('Rain Delay Restrictions', 'mdi:cancel'), + TYPE_RAINSENSOR: ('Rain Sensor Restrictions', 'mdi:cancel'), + TYPE_WEEKDAY: ('Weekday Restrictions', 'mdi:cancel'), +} + +SENSORS = { + TYPE_FREEZE_TEMP: ('Freeze Protect Temperature', 'mdi:thermometer', '°C'), +} + +BINARY_SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]) +}) + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) +}) + +SERVICE_START_PROGRAM_SCHEMA = vol.Schema({ + vol.Required(CONF_PROGRAM_ID): cv.positive_int, +}) + +SERVICE_START_ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONE_ID): cv.positive_int, + vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN): + cv.positive_int, +}) + +SERVICE_STOP_PROGRAM_SCHEMA = vol.Schema({ + vol.Required(CONF_PROGRAM_ID): cv.positive_int, +}) + +SERVICE_STOP_ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONE_ID): cv.positive_int, +}) + +SWITCH_SCHEMA = vol.Schema({vol.Optional(CONF_ZONE_RUN_TIME): cv.positive_int}) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: + vol.Schema({ + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_BINARY_SENSORS, default={}): + BINARY_SENSOR_SCHEMA, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA, + }) + }, + extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the RainMachine component.""" + from regenmaschine import Authenticator, Client + from regenmaschine.exceptions import RainMachineError + + conf = config[DOMAIN] + ip_address = conf[CONF_IP_ADDRESS] + password = conf[CONF_PASSWORD] + port = conf[CONF_PORT] + ssl = conf[CONF_SSL] + + try: + auth = Authenticator.create_local( + ip_address, password, port=port, https=ssl) + rainmachine = RainMachine(hass, Client(auth)) + rainmachine.update() + hass.data[DATA_RAINMACHINE] = rainmachine + except RainMachineError as exc: + _LOGGER.error('An error occurred: %s', str(exc)) + hass.components.persistent_notification.create( + 'Error: {0}
' + 'You will need to restart hass after fixing.' + ''.format(exc), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + for component, schema in [ + ('binary_sensor', conf[CONF_BINARY_SENSORS]), + ('sensor', conf[CONF_SENSORS]), + ('switch', conf[CONF_SWITCHES]), + ]: + discovery.load_platform(hass, component, DOMAIN, schema, config) + + def refresh(event_time): + """Refresh RainMachine data.""" + _LOGGER.debug('Updating RainMachine data') + hass.data[DATA_RAINMACHINE].update() + dispatcher_send(hass, DATA_UPDATE_TOPIC) + + track_time_interval(hass, refresh, DEFAULT_SCAN_INTERVAL) + + def start_program(service): + """Start a particular program.""" + rainmachine.client.programs.start(service.data[CONF_PROGRAM_ID]) + + def start_zone(service): + """Start a particular zone for a certain amount of time.""" + rainmachine.client.zones.start(service.data[CONF_ZONE_ID], + service.data[CONF_ZONE_RUN_TIME]) + + def stop_all(service): + """Stop all watering.""" + rainmachine.client.watering.stop_all() + + def stop_program(service): + """Stop a program.""" + rainmachine.client.programs.stop(service.data[CONF_PROGRAM_ID]) + + def stop_zone(service): + """Stop a zone.""" + rainmachine.client.zones.stop(service.data[CONF_ZONE_ID]) + + for service, method, schema in [ + ('start_program', start_program, SERVICE_START_PROGRAM_SCHEMA), + ('start_zone', start_zone, SERVICE_START_ZONE_SCHEMA), + ('stop_all', stop_all, {}), + ('stop_program', stop_program, SERVICE_STOP_PROGRAM_SCHEMA), + ('stop_zone', stop_zone, SERVICE_STOP_ZONE_SCHEMA) + ]: + hass.services.register(DOMAIN, service, method, schema=schema) + + return True + + +class RainMachine(object): + """Define a generic RainMachine object.""" + + def __init__(self, hass, client): + """Initialize.""" + self.client = client + self.device_mac = self.client.provision.wifi()['macAddress'] + self.restrictions = {} + + def update(self): + """Update sensor/binary sensor data.""" + self.restrictions.update({ + 'current': self.client.restrictions.current(), + 'global': self.client.restrictions.universal() + }) + + +class RainMachineEntity(Entity): + """Define a generic RainMachine entity.""" + + def __init__(self, rainmachine): + """Initialize.""" + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._name = None + self.rainmachine = rainmachine + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes.""" + return self._attrs + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml new file mode 100644 index 00000000000..a8c77628c8f --- /dev/null +++ b/homeassistant/components/rainmachine/services.yaml @@ -0,0 +1,32 @@ +# Describes the format for available RainMachine services + +--- +start_program: + description: Start a program. + fields: + program_id: + description: The program to start. + example: 3 +start_zone: + description: Start a zone for a set number of seconds. + fields: + zone_id: + description: The zone to start. + example: 3 + zone_run_time: + description: The number of seconds to run the zone. + example: 120 +stop_all: + description: Stop all watering activities. +stop_program: + description: Stop a program. + fields: + program_id: + description: The program to stop. + example: 3 +stop_zone: + description: Stop a zone. + fields: + zone_id: + description: The zone to stop. + example: 3 diff --git a/homeassistant/components/sensor/rainmachine.py b/homeassistant/components/sensor/rainmachine.py new file mode 100644 index 00000000000..8faf30acc38 --- /dev/null +++ b/homeassistant/components/sensor/rainmachine.py @@ -0,0 +1,88 @@ +""" +This platform provides support for sensor data from RainMachine. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.rainmachine/ +""" +import logging + +from homeassistant.components.rainmachine import ( + DATA_RAINMACHINE, DATA_UPDATE_TOPIC, SENSORS, RainMachineEntity) +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['rainmachine'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the RainMachine Switch platform.""" + if discovery_info is None: + return + + rainmachine = hass.data[DATA_RAINMACHINE] + + sensors = [] + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + name, icon, unit = SENSORS[sensor_type] + sensors.append( + RainMachineSensor(rainmachine, sensor_type, name, icon, unit)) + + add_devices(sensors, True) + + +class RainMachineSensor(RainMachineEntity): + """A sensor implementation for raincloud device.""" + + def __init__(self, rainmachine, sensor_type, name, icon, unit): + """Initialize.""" + super().__init__(rainmachine) + + self._icon = icon + self._name = name + self._sensor_type = sensor_type + self._state = None + self._unit = unit + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def state(self) -> str: + """Return the name of the entity.""" + return self._state + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}'.format( + self.rainmachine.device_mac.replace(':', ''), self._sensor_type) + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @callback + def update_data(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC, + self.update_data) + + def update(self): + """Update the sensor's state.""" + self._state = self.rainmachine.restrictions['global'][ + 'freezeProtectTemp'] diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index beb00eeca44..f4b2d780a9a 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -4,12 +4,11 @@ This component provides support for RainMachine programs and zones. For more details about this component, please refer to the documentation at https://home-assistant.io/components/switch.rainmachine/ """ - -from logging import getLogger +import logging from homeassistant.components.rainmachine import ( - CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, PROGRAM_UPDATE_TOPIC, - RainMachineEntity) + CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ZONE_RUN, + PROGRAM_UPDATE_TOPIC, RainMachineEntity) from homeassistant.const import ATTR_ID from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback @@ -18,7 +17,7 @@ from homeassistant.helpers.dispatcher import ( DEPENDENCIES = ['rainmachine'] -_LOGGER = getLogger(__name__) +_LOGGER = logging.getLogger(__name__) ATTR_AREA = 'area' ATTR_CS_ON = 'cs_on' @@ -39,8 +38,6 @@ ATTR_SUN_EXPOSURE = 'sun_exposure' ATTR_VEGETATION_TYPE = 'vegetation_type' ATTR_ZONES = 'zones' -DEFAULT_ZONE_RUN = 60 * 10 - DAYS = [ 'Monday', 'Tuesday', @@ -141,26 +138,41 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RainMachineSwitch(RainMachineEntity, SwitchDevice): - """A class to represent a generic RainMachine entity.""" + """A class to represent a generic RainMachine switch.""" - def __init__(self, rainmachine, rainmachine_type, obj): - """Initialize a generic RainMachine entity.""" + def __init__(self, rainmachine, switch_type, obj): + """Initialize a generic RainMachine switch.""" + super().__init__(rainmachine) + + self._name = obj['name'] self._obj = obj - self._type = rainmachine_type + self._rainmachine_entity_id = obj['uid'] + self._switch_type = switch_type - super().__init__(rainmachine, rainmachine_type, obj.get('uid')) + @property + def icon(self) -> str: + """Return the icon.""" + return 'mdi:water' @property def is_enabled(self) -> bool: """Return whether the entity is enabled.""" return self._obj.get('active') + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}_{2}'.format( + self.rainmachine.device_mac.replace(':', ''), + self._switch_type, + self._rainmachine_entity_id) + class RainMachineProgram(RainMachineSwitch): """A RainMachine program.""" def __init__(self, rainmachine, obj): - """Initialize.""" + """Initialize a generic RainMachine switch.""" super().__init__(rainmachine, 'program', obj) @property @@ -168,11 +180,6 @@ class RainMachineProgram(RainMachineSwitch): """Return whether the program is running.""" return bool(self._obj.get('status')) - @property - def name(self) -> str: - """Return the name of the program.""" - return 'Program: {0}'.format(self._obj.get('name')) - @property def zones(self) -> list: """Return a list of active zones associated with this program.""" @@ -180,29 +187,29 @@ class RainMachineProgram(RainMachineSwitch): def turn_off(self, **kwargs) -> None: """Turn the program off.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.programs.stop(self._rainmachine_entity_id) dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to turn off program "%s"', self.unique_id) _LOGGER.debug(exc_info) def turn_on(self, **kwargs) -> None: """Turn the program on.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.programs.start(self._rainmachine_entity_id) dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to turn on program "%s"', self.unique_id) _LOGGER.debug(exc_info) def update(self) -> None: """Update info for the program.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self._obj = self.rainmachine.client.programs.get( @@ -210,16 +217,11 @@ class RainMachineProgram(RainMachineSwitch): self._attrs.update({ ATTR_ID: self._obj['uid'], - ATTR_CS_ON: self._obj.get('cs_on'), - ATTR_CYCLES: self._obj.get('cycles'), - ATTR_DELAY: self._obj.get('delay'), - ATTR_DELAY_ON: self._obj.get('delay_on'), ATTR_SOAK: self._obj.get('soak'), - ATTR_STATUS: - PROGRAM_STATUS_MAP[self._obj.get('status')], + ATTR_STATUS: PROGRAM_STATUS_MAP[self._obj.get('status')], ATTR_ZONES: ', '.join(z['name'] for z in self.zones) }) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to update info for program "%s"', self.unique_id) _LOGGER.debug(exc_info) @@ -240,11 +242,6 @@ class RainMachineZone(RainMachineSwitch): """Return whether the zone is running.""" return bool(self._obj.get('state')) - @property - def name(self) -> str: - """Return the name of the zone.""" - return 'Zone: {0}'.format(self._obj.get('name')) - @callback def _program_updated(self): """Update state, trigger updates.""" @@ -257,28 +254,28 @@ class RainMachineZone(RainMachineSwitch): def turn_off(self, **kwargs) -> None: """Turn the zone off.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.zones.stop(self._rainmachine_entity_id) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to turn off zone "%s"', self.unique_id) _LOGGER.debug(exc_info) def turn_on(self, **kwargs) -> None: """Turn the zone on.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.zones.start(self._rainmachine_entity_id, self._run_time) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to turn on zone "%s"', self.unique_id) _LOGGER.debug(exc_info) def update(self) -> None: """Update info for the zone.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self._obj = self.rainmachine.client.zones.get( @@ -309,7 +306,7 @@ class RainMachineZone(RainMachineSwitch): ATTR_VEGETATION_TYPE: VEGETATION_MAP[self._obj.get('type')], }) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to update info for zone "%s"', self.unique_id) _LOGGER.debug(exc_info) diff --git a/requirements_all.txt b/requirements_all.txt index 958b0f1027e..1b1db52daef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1147,7 +1147,7 @@ raincloudy==0.0.4 # raspihats==2.2.3 # homeassistant.components.rainmachine -regenmaschine==0.4.1 +regenmaschine==0.4.2 # homeassistant.components.python_script restrictedpython==4.0b4 From 7d2563eb1f899a908c88811abe9d84c6730db1b8 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 29 May 2018 22:43:26 +0200 Subject: [PATCH 085/137] Update HAP-python to 2.2.2 (#14674) * Pass driver to accessory * Added 'hk_driver' fixture for tests --- homeassistant/components/homekit/__init__.py | 16 ++++---- .../components/homekit/accessories.py | 12 +++--- .../components/homekit/type_switches.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/conftest.py | 16 ++++++++ tests/components/homekit/test_accessories.py | 21 ++++++---- .../homekit/test_get_accessories.py | 23 ++++++----- tests/components/homekit/test_homekit.py | 41 +++++++++++-------- tests/components/homekit/test_type_covers.py | 16 ++++---- tests/components/homekit/test_type_fans.py | 12 +++--- tests/components/homekit/test_type_lights.py | 16 ++++---- tests/components/homekit/test_type_locks.py | 8 ++-- .../homekit/test_type_media_players.py | 4 +- .../homekit/test_type_security_systems.py | 10 +++-- tests/components/homekit/test_type_sensors.py | 31 +++++++------- .../components/homekit/test_type_switches.py | 4 +- .../homekit/test_type_thermostats.py | 16 ++++---- 18 files changed, 143 insertions(+), 109 deletions(-) create mode 100644 tests/components/homekit/conftest.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index a79fbf85400..ce3b79e6c72 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -30,7 +30,7 @@ from .util import ( TYPES = Registry() _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==2.1.0'] +REQUIREMENTS = ['HAP-python==2.2.2'] # #### Driver Status #### STATUS_READY = 0 @@ -84,7 +84,7 @@ async def async_setup(hass, config): return True -def get_accessory(hass, state, aid, config): +def get_accessory(hass, driver, state, aid, config): """Take state and return an accessory object if supported.""" if not aid: _LOGGER.warning('The entitiy "%s" is not supported, since it ' @@ -157,7 +157,7 @@ def get_accessory(hass, state, aid, config): return None _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type) - return TYPES[a_type](hass, name, state.entity_id, aid, config) + return TYPES[a_type](hass, driver, name, state.entity_id, aid, config) def generate_aid(entity_id): @@ -192,9 +192,9 @@ class HomeKit(): ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) - self.bridge = HomeBridge(self.hass) - self.driver = HomeDriver(self.hass, self.bridge, port=self._port, - address=ip_addr, persist_file=path) + self.driver = HomeDriver(self.hass, address=ip_addr, + port=self._port, persist_file=path) + self.bridge = HomeBridge(self.hass, self.driver) def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" @@ -202,7 +202,7 @@ class HomeKit(): return aid = generate_aid(state.entity_id) conf = self._config.pop(state.entity_id, {}) - acc = get_accessory(self.hass, state, aid, conf) + acc = get_accessory(self.hass, self.driver, state, aid, conf) if acc is not None: self.bridge.add_accessory(acc) @@ -220,7 +220,7 @@ class HomeKit(): for state in self.hass.states.all(): self.add_bridge_accessory(state) - self.bridge.set_driver(self.driver) + self.driver.add_accessory(self.bridge) if not self.driver.state.paired: show_setup_message(self.hass, self.driver.state.pincode) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ded4526b008..711bf0030f0 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -64,10 +64,10 @@ def debounce(func): class HomeAccessory(Accessory): """Adapter class for Accessory.""" - def __init__(self, hass, name, entity_id, aid, config, + def __init__(self, hass, driver, name, entity_id, aid, config, category=CATEGORY_OTHER): """Initialize a Accessory object.""" - super().__init__(name, aid=aid) + super().__init__(driver, name, aid=aid) model = split_entity_id(entity_id)[0].replace("_", " ").title() self.set_info_service( firmware_revision=__version__, manufacturer=MANUFACTURER, @@ -104,9 +104,9 @@ class HomeAccessory(Accessory): class HomeBridge(Bridge): """Adapter class for Bridge.""" - def __init__(self, hass, name=BRIDGE_NAME): + def __init__(self, hass, driver, name=BRIDGE_NAME): """Initialize a Bridge object.""" - super().__init__(name) + super().__init__(driver, name) self.set_info_service( firmware_revision=__version__, manufacturer=MANUFACTURER, model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER) @@ -120,9 +120,9 @@ class HomeBridge(Bridge): class HomeDriver(AccessoryDriver): """Adapter class for AccessoryDriver.""" - def __init__(self, hass, *args, **kwargs): + def __init__(self, hass, **kwargs): """Initialize a AccessoryDriver object.""" - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self.hass = hass def pair(self, client_uuid, client_public): diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 5754266587c..69f14821bd6 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -19,7 +19,7 @@ class Switch(HomeAccessory): """Generate a Switch accessory.""" def __init__(self, *args): - """Initialize a Switch accessory object to represent a remote.""" + """Initialize a Switch accessory object.""" super().__init__(*args, category=CATEGORY_SWITCH) self._domain = split_entity_id(self.entity_id)[0] self.flag_target_state = False diff --git a/requirements_all.txt b/requirements_all.txt index 1b1db52daef..3a190a29ed8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -28,7 +28,7 @@ Adafruit-SHT31==1.0.2 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==2.1.0 +HAP-python==2.2.2 # homeassistant.components.notify.mastodon Mastodon.py==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index adcba607db0..56b7eec6c5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.5 # homeassistant.components.homekit -HAP-python==2.1.0 +HAP-python==2.2.2 # homeassistant.components.notify.html5 PyJWT==1.6.0 diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py new file mode 100644 index 00000000000..f7839265939 --- /dev/null +++ b/tests/components/homekit/conftest.py @@ -0,0 +1,16 @@ +"""HomeKit session fixtures.""" +from unittest.mock import patch + +import pytest + +from pyhap.accessory_driver import AccessoryDriver + + +@pytest.fixture(scope='session') +def hk_driver(): + """Return a custom AccessoryDriver instance for HomeKit accessory init.""" + with patch('pyhap.accessory_driver.Zeroconf'), \ + patch('pyhap.accessory_driver.AccessoryEncoder'), \ + patch('pyhap.accessory_driver.HAPServer'), \ + patch('pyhap.accessory_driver.AccessoryDriver.publish'): + return AccessoryDriver(pincode=b'123-45-678') diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 3d1c335f8ae..a0764d58000 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -50,13 +50,14 @@ async def test_debounce(hass): assert counter == 2 -async def test_home_accessory(hass): +async def test_home_accessory(hass, hk_driver): """Test HomeAccessory class.""" entity_id = 'homekit.accessory' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = HomeAccessory(hass, 'Home Accessory', entity_id, 2, None) + acc = HomeAccessory(hass, hk_driver, 'Home Accessory', + entity_id, 2, None) assert acc.hass == hass assert acc.display_name == 'Home Accessory' assert acc.aid == 2 @@ -86,14 +87,15 @@ async def test_home_accessory(hass): acc.update_state('new_state') # Test model name from domain - acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, None) + entity_id = 'test_model.demo' + acc = HomeAccessory('hass', hk_driver, 'test_name', entity_id, 2, None) serv = acc.services[0] # SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_MODEL).value == 'Test Model' -def test_home_bridge(): +def test_home_bridge(hk_driver): """Test HomeBridge class.""" - bridge = HomeBridge('hass') + bridge = HomeBridge('hass', hk_driver) assert bridge.hass == 'hass' assert bridge.display_name == BRIDGE_NAME assert bridge.category == 2 # Category.BRIDGE @@ -107,7 +109,7 @@ def test_home_bridge(): assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == \ BRIDGE_SERIAL_NUMBER - bridge = HomeBridge('hass', 'test_name') + bridge = HomeBridge('hass', hk_driver, 'test_name') assert bridge.display_name == 'test_name' assert len(bridge.services) == 1 serv = bridge.services[0] # SERV_ACCESSORY_INFO @@ -118,7 +120,6 @@ def test_home_bridge(): def test_home_driver(): """Test HomeDriver class.""" - bridge = HomeBridge('hass') ip_address = '127.0.0.1' port = 51826 path = '.homekit.state' @@ -126,9 +127,11 @@ def test_home_driver(): with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ as mock_driver: - driver = HomeDriver('hass', bridge, ip_address, port, path) + driver = HomeDriver('hass', address=ip_address, port=port, + persist_file=path) - mock_driver.assert_called_with(bridge, ip_address, port, path) + mock_driver.assert_called_with(address=ip_address, port=port, + persist_file=path) driver.state = Mock(pincode=pin) # pair diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 46e5f8b1174..3b7f307fce7 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -18,10 +18,12 @@ from homeassistant.const import ( def test_not_supported(caplog): """Test if none is returned if entity isn't supported.""" # not supported entity - assert get_accessory(None, State('demo.demo', 'on'), 2, {}) is None + assert get_accessory(None, None, State('demo.demo', 'on'), 2, {}) \ + is None # invalid aid - assert get_accessory(None, State('light.demo', 'on'), None, None) is None + assert get_accessory(None, None, State('light.demo', 'on'), None, None) \ + is None assert caplog.records[0].levelname == 'WARNING' assert 'invalid aid' in caplog.records[0].msg @@ -31,11 +33,11 @@ def test_not_supported_media_player(): # selected mode for entity not supported config = {CONF_FEATURE_LIST: {FEATURE_ON_OFF: None}} entity_state = State('media_player.demo', 'on') - get_accessory(None, entity_state, 2, config) is None + get_accessory(None, None, entity_state, 2, config) is None # no supported modes for entity entity_state = State('media_player.demo', 'on') - assert get_accessory(None, entity_state, 2, {}) is None + assert get_accessory(None, None, entity_state, 2, {}) is None @pytest.mark.parametrize('config, name', [ @@ -46,8 +48,9 @@ def test_customize_options(config, name): mock_type = Mock() with patch.dict(TYPES, {'Light': mock_type}): entity_state = State('light.demo', 'on') - get_accessory(None, entity_state, 2, config) - mock_type.assert_called_with(None, name, 'light.demo', 2, config) + get_accessory(None, None, entity_state, 2, config) + mock_type.assert_called_with(None, None, name, + 'light.demo', 2, config) @pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ @@ -70,7 +73,7 @@ def test_types(type_name, entity_id, state, attrs, config): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, config) + get_accessory(None, None, entity_state, 2, config) assert mock_type.called if config: @@ -91,7 +94,7 @@ def test_type_covers(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, {}) + get_accessory(None, None, entity_state, 2, {}) assert mock_type.called @@ -122,7 +125,7 @@ def test_type_sensors(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, {}) + get_accessory(None, None, entity_state, 2, {}) assert mock_type.called @@ -138,5 +141,5 @@ def test_type_switches(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, {}) + get_accessory(None, None, entity_state, 2, {}) assert mock_type.called diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 31337088b33..08e8da7857e 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -94,11 +94,12 @@ async def test_setup_auto_start_disabled(hass): assert homekit.start.called is False -async def test_homekit_setup(hass): +async def test_homekit_setup(hass, hk_driver): """Test setup of bridge and driver.""" homekit = HomeKit(hass, DEFAULT_PORT, None, {}, {}) - with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ + with patch(PATH_HOMEKIT + '.accessories.HomeDriver', + return_value=hk_driver) as mock_driver, \ patch('homeassistant.util.get_local_ip') as mock_ip: mock_ip.return_value = IP_ADDRESS await hass.async_add_job(homekit.setup) @@ -106,41 +107,42 @@ async def test_homekit_setup(hass): path = hass.config.path(HOMEKIT_FILE) assert isinstance(homekit.bridge, HomeBridge) mock_driver.assert_called_with( - hass, homekit.bridge, port=DEFAULT_PORT, - address=IP_ADDRESS, persist_file=path) + hass, address=IP_ADDRESS, port=DEFAULT_PORT, persist_file=path) # Test if stop listener is setup assert hass.bus.async_listeners().get(EVENT_HOMEASSISTANT_STOP) == 1 -async def test_homekit_setup_ip_address(hass): +async def test_homekit_setup_ip_address(hass, hk_driver): """Test setup with given IP address.""" homekit = HomeKit(hass, DEFAULT_PORT, '172.0.0.0', {}, {}) - with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: + with patch(PATH_HOMEKIT + '.accessories.HomeDriver', + return_value=hk_driver) as mock_driver: await hass.async_add_job(homekit.setup) mock_driver.assert_called_with( - hass, ANY, port=DEFAULT_PORT, address='172.0.0.0', persist_file=ANY) + hass, address='172.0.0.0', port=DEFAULT_PORT, persist_file=ANY) async def test_homekit_add_accessory(): """Add accessory if config exists and get_acc returns an accessory.""" homekit = HomeKit('hass', None, None, lambda entity_id: True, {}) + homekit.driver = 'driver' homekit.bridge = mock_bridge = Mock() with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: mock_get_acc.side_effect = [None, 'acc', None] homekit.add_bridge_accessory(State('light.demo', 'on')) - mock_get_acc.assert_called_with('hass', ANY, 363398124, {}) + mock_get_acc.assert_called_with('hass', 'driver', ANY, 363398124, {}) assert not mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test', 'on')) - mock_get_acc.assert_called_with('hass', ANY, 294192020, {}) + mock_get_acc.assert_called_with('hass', 'driver', ANY, 294192020, {}) assert mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test_2', 'on')) - mock_get_acc.assert_called_with('hass', ANY, 429982757, {}) + mock_get_acc.assert_called_with('hass', 'driver', ANY, 429982757, {}) mock_bridge.add_accessory.assert_called_with('acc') @@ -164,30 +166,35 @@ async def test_homekit_entity_filter(hass): assert mock_get_acc.called is False -async def test_homekit_start(hass, debounce_patcher): +async def test_homekit_start(hass, hk_driver, debounce_patcher): """Test HomeKit start method.""" pin = b'123-45-678' homekit = HomeKit(hass, None, None, {}, {'cover.demo': {}}) - homekit.bridge = Mock() - homekit.driver = mock_driver = Mock(state=Mock(paired=False, pincode=pin)) + homekit.bridge = 'bridge' + homekit.driver = hk_driver hass.states.async_set('light.demo', 'on') state = hass.states.async_all()[0] with patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') as \ mock_add_acc, \ - patch(PATH_HOMEKIT + '.show_setup_message') as mock_setup_msg: + patch(PATH_HOMEKIT + '.show_setup_message') as mock_setup_msg, \ + patch('pyhap.accessory_driver.AccessoryDriver.add_accessory') as \ + hk_driver_add_acc, \ + patch('pyhap.accessory_driver.AccessoryDriver.start') as \ + hk_driver_start: await hass.async_add_job(homekit.start) mock_add_acc.assert_called_with(state) mock_setup_msg.assert_called_with(hass, pin) - assert mock_driver.start.called is True + hk_driver_add_acc.assert_called_with('bridge') + assert hk_driver_start.called assert homekit.status == STATUS_RUNNING # Test start() if already started - mock_driver.reset_mock() + hk_driver_start.reset_mock() await hass.async_add_job(homekit.start) - assert mock_driver.start.called is False + assert not hk_driver_start.called async def test_homekit_stop(hass): diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 8138d1c506b..c69ddacd328 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -28,13 +28,13 @@ def cls(): patcher.stop() -async def test_garage_door_open_close(hass, cls): +async def test_garage_door_open_close(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.garage_door' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = cls.garage(hass, 'Garage Door', entity_id, 2, None) + acc = cls.garage(hass, hk_driver, 'Garage Door', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -85,13 +85,13 @@ async def test_garage_door_open_close(hass, cls): assert acc.char_target_state.value == 0 -async def test_window_set_cover_position(hass, cls): +async def test_window_set_cover_position(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = cls.window(hass, 'Cover', entity_id, 2, None) + acc = cls.window(hass, hk_driver, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -133,13 +133,13 @@ async def test_window_set_cover_position(hass, cls): assert acc.char_target_position.value == 75 -async def test_window_open_close(hass, cls): +async def test_window_open_close(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0}) - acc = cls.window_basic(hass, 'Cover', entity_id, 2, None) + acc = cls.window_basic(hass, hk_driver, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -196,13 +196,13 @@ async def test_window_open_close(hass, cls): assert acc.char_position_state.value == 2 -async def test_window_open_close_stop(hass, cls): +async def test_window_open_close_stop(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) - acc = cls.window_basic(hass, 'Cover', entity_id, 2, None) + acc = cls.window_basic(hass, hk_driver, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) # Set from HomeKit diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index f96fe19d603..ba7d4ccdcf0 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -27,14 +27,14 @@ def cls(): patcher.stop() -async def test_fan_basic(hass, cls): +async def test_fan_basic(hass, hk_driver, cls): """Test fan with char state.""" entity_id = 'fan.demo' hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.fan(hass, 'Fan', entity_id, 2, None) + acc = cls.fan(hass, hk_driver, 'Fan', entity_id, 2, None) assert acc.aid == 2 assert acc.category == 3 # Fan @@ -75,7 +75,7 @@ async def test_fan_basic(hass, cls): assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id -async def test_fan_direction(hass, cls): +async def test_fan_direction(hass, hk_driver, cls): """Test fan with direction.""" entity_id = 'fan.demo' @@ -83,7 +83,7 @@ async def test_fan_direction(hass, cls): ATTR_SUPPORTED_FEATURES: SUPPORT_DIRECTION, ATTR_DIRECTION: DIRECTION_FORWARD}) await hass.async_block_till_done() - acc = cls.fan(hass, 'Fan', entity_id, 2, None) + acc = cls.fan(hass, hk_driver, 'Fan', entity_id, 2, None) assert acc.char_direction.value == 0 @@ -113,14 +113,14 @@ async def test_fan_direction(hass, cls): assert call_set_direction[1].data[ATTR_DIRECTION] == DIRECTION_REVERSE -async def test_fan_oscillate(hass, cls): +async def test_fan_oscillate(hass, hk_driver, cls): """Test fan with oscillate.""" entity_id = 'fan.demo' hass.states.async_set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_OSCILLATE, ATTR_OSCILLATING: False}) await hass.async_block_till_done() - acc = cls.fan(hass, 'Fan', entity_id, 2, None) + acc = cls.fan(hass, hk_driver, 'Fan', entity_id, 2, None) assert acc.char_swing.value == 0 diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 7a1db7b3f71..a9a5f1c3ece 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -26,14 +26,14 @@ def cls(): patcher.stop() -async def test_light_basic(hass, cls): +async def test_light_basic(hass, hk_driver, cls): """Test light with char state.""" entity_id = 'light.demo' hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, None) + acc = cls.light(hass, hk_driver, 'Light', entity_id, 2, None) assert acc.aid == 2 assert acc.category == 5 # Lightbulb @@ -74,14 +74,14 @@ async def test_light_basic(hass, cls): assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id -async def test_light_brightness(hass, cls): +async def test_light_brightness(hass, hk_driver, cls): """Test light with brightness.""" entity_id = 'light.demo' hass.states.async_set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, None) + acc = cls.light(hass, hk_driver, 'Light', entity_id, 2, None) assert acc.char_brightness.value == 0 @@ -118,7 +118,7 @@ async def test_light_brightness(hass, cls): assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id -async def test_light_color_temperature(hass, cls): +async def test_light_color_temperature(hass, hk_driver, cls): """Test light with color temperature.""" entity_id = 'light.demo' @@ -126,7 +126,7 @@ async def test_light_color_temperature(hass, cls): ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, None) + acc = cls.light(hass, hk_driver, 'Light', entity_id, 2, None) assert acc.char_color_temperature.value == 153 @@ -145,7 +145,7 @@ async def test_light_color_temperature(hass, cls): assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 -async def test_light_rgb_color(hass, cls): +async def test_light_rgb_color(hass, hk_driver, cls): """Test light with rgb_color.""" entity_id = 'light.demo' @@ -153,7 +153,7 @@ async def test_light_rgb_color(hass, cls): ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, None) + acc = cls.light(hass, hk_driver, 'Light', entity_id, 2, None) assert acc.char_hue.value == 0 assert acc.char_saturation.value == 75 diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index f4698b1380b..8f18a591019 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -9,7 +9,7 @@ from homeassistant.const import ( from tests.common import async_mock_service -async def test_lock_unlock(hass): +async def test_lock_unlock(hass, hk_driver): """Test if accessory and HA are updated accordingly.""" code = '1234' config = {ATTR_CODE: code} @@ -17,7 +17,7 @@ async def test_lock_unlock(hass): hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Lock(hass, 'Lock', entity_id, 2, config) + acc = Lock(hass, hk_driver, 'Lock', entity_id, 2, config) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -66,13 +66,13 @@ async def test_lock_unlock(hass): @pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) -async def test_no_code(hass, config): +async def test_no_code(hass, hk_driver, config): """Test accessory if lock doesn't require a code.""" entity_id = 'lock.kitchen_door' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Lock(hass, 'Lock', entity_id, 2, config) + acc = Lock(hass, hk_driver, 'Lock', entity_id, 2, config) # Set from HomeKit call_lock = async_mock_service(hass, DOMAIN, 'lock') diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index d89f9740ea6..4076b1f8a89 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -15,7 +15,7 @@ from homeassistant.const import ( from tests.common import async_mock_service -async def test_media_player_set_state(hass): +async def test_media_player_set_state(hass, hk_driver): """Test if accessory and HA are updated accordingly.""" config = {CONF_FEATURE_LIST: { FEATURE_ON_OFF: None, FEATURE_PLAY_PAUSE: None, @@ -25,7 +25,7 @@ async def test_media_player_set_state(hass): hass.states.async_set(entity_id, None, {ATTR_SUPPORTED_FEATURES: 20873, ATTR_MEDIA_VOLUME_MUTED: False}) await hass.async_block_till_done() - acc = MediaPlayer(hass, 'MediaPlayer', entity_id, 2, config) + acc = MediaPlayer(hass, hk_driver, 'MediaPlayer', entity_id, 2, config) await hass.async_add_job(acc.run) assert acc.aid == 2 diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 7b72404cdaa..3ddce0f36eb 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -12,7 +12,7 @@ from homeassistant.const import ( from tests.common import async_mock_service -async def test_switch_set_state(hass): +async def test_switch_set_state(hass, hk_driver): """Test if accessory and HA are updated accordingly.""" code = '1234' config = {ATTR_CODE: code} @@ -20,7 +20,8 @@ async def test_switch_set_state(hass): hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) + acc = SecuritySystem(hass, hk_driver, 'SecuritySystem', + entity_id, 2, config) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -95,13 +96,14 @@ async def test_switch_set_state(hass): @pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) -async def test_no_alarm_code(hass, config): +async def test_no_alarm_code(hass, hk_driver, config): """Test accessory if security_system doesn't require an alarm_code.""" entity_id = 'alarm_control_panel.test' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) + acc = SecuritySystem(hass, hk_driver, 'SecuritySystem', + entity_id, 2, config) # Set from HomeKit call_arm_home = async_mock_service(hass, DOMAIN, 'alarm_arm_home') diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index e36ae67da12..54ecbcb196f 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -8,13 +8,14 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) -async def test_temperature(hass): +async def test_temperature(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.temperature' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = TemperatureSensor(hass, 'Temperature', entity_id, 2, None) + acc = TemperatureSensor(hass, hk_driver, 'Temperature', + entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -40,13 +41,13 @@ async def test_temperature(hass): assert acc.char_temp.value == 24 -async def test_humidity(hass): +async def test_humidity(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.humidity' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = HumiditySensor(hass, 'Humidity', entity_id, 2, None) + acc = HumiditySensor(hass, hk_driver, 'Humidity', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -63,13 +64,14 @@ async def test_humidity(hass): assert acc.char_humidity.value == 20 -async def test_air_quality(hass): +async def test_air_quality(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.air_quality' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = AirQualitySensor(hass, 'Air Quality', entity_id, 2, None) + acc = AirQualitySensor(hass, hk_driver, 'Air Quality', + entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -94,13 +96,13 @@ async def test_air_quality(hass): assert acc.char_quality.value == 5 -async def test_co2(hass): +async def test_co2(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.co2' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = CarbonDioxideSensor(hass, 'CO2', entity_id, 2, None) + acc = CarbonDioxideSensor(hass, hk_driver, 'CO2', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -129,13 +131,13 @@ async def test_co2(hass): assert acc.char_detected.value == 0 -async def test_light(hass): +async def test_light(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.light' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = LightSensor(hass, 'Light', entity_id, 2, None) + acc = LightSensor(hass, hk_driver, 'Light', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -152,7 +154,7 @@ async def test_light(hass): assert acc.char_light.value == 300 -async def test_binary(hass): +async def test_binary(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'binary_sensor.opening' @@ -160,7 +162,7 @@ async def test_binary(hass): {ATTR_DEVICE_CLASS: 'opening'}) await hass.async_block_till_done() - acc = BinarySensor(hass, 'Window Opening', entity_id, 2, None) + acc = BinarySensor(hass, hk_driver, 'Window Opening', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -193,7 +195,7 @@ async def test_binary(hass): assert acc.char_detected.value == 0 -async def test_binary_device_classes(hass): +async def test_binary_device_classes(hass, hk_driver): """Test if services and characteristics are assigned correctly.""" entity_id = 'binary_sensor.demo' @@ -202,6 +204,7 @@ async def test_binary_device_classes(hass): {ATTR_DEVICE_CLASS: device_class}) await hass.async_block_till_done() - acc = BinarySensor(hass, 'Binary Sensor', entity_id, 2, None) + acc = BinarySensor(hass, hk_driver, 'Binary Sensor', + entity_id, 2, None) assert acc.get_service(service).display_name == service assert acc.char_detected.display_name == char diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index ff94c4b6a0b..b1830d1926f 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -15,13 +15,13 @@ from tests.common import async_mock_service 'script.test', 'switch.test', ]) -async def test_switch_set_state(hass, entity_id): +async def test_switch_set_state(hass, hk_driver, entity_id): """Test if accessory and HA are updated accordingly.""" domain = split_entity_id(entity_id)[0] hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Switch(hass, 'Switch', entity_id, 2, None) + acc = Switch(hass, hk_driver, 'Switch', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 337ad23ad05..6d6a48c7971 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -27,13 +27,13 @@ def cls(): patcher.stop() -async def test_default_thermostat(hass, cls): +async def test_default_thermostat(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -166,14 +166,14 @@ async def test_default_thermostat(hass, cls): assert acc.char_target_heat_cool.value == 1 -async def test_auto_thermostat(hass, cls): +async def test_auto_thermostat(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' # support_auto = True hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.char_cooling_thresh_temp.value == 23.0 @@ -241,7 +241,7 @@ async def test_auto_thermostat(hass, cls): assert acc.char_cooling_thresh_temp.value == 25.0 -async def test_power_state(hass, cls): +async def test_power_state(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' @@ -252,7 +252,7 @@ async def test_power_state(hass, cls): ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.support_power_state is True @@ -297,14 +297,14 @@ async def test_power_state(hass, cls): assert acc.char_target_heat_cool.value == 0 -async def test_thermostat_fahrenheit(hass, cls): +async def test_thermostat_fahrenheit(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' # support_auto = True hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) hass.states.async_set(entity_id, STATE_AUTO, From e746b92e0e7111043746bbd45ac4312cc457361b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 29 May 2018 17:14:58 -0400 Subject: [PATCH 086/137] Fix deprecated code (#14681) --- homeassistant/components/cloud/iot.py | 2 +- tests/components/cloud/test_iot.py | 2 +- tests/components/test_conversation.py | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index 7cf8e50e866..12b81c9003b 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -185,7 +185,7 @@ class CloudIoT: yield from client.send_json(response) except client_exceptions.WSServerHandshakeError as err: - if err.code == 401: + if err.status == 401: disconnect_warn = 'Invalid auth.' self.close_requested = True # Should we notify user? diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index 81b1e315085..1b580d0eb9b 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -210,7 +210,7 @@ def test_cloud_connect_invalid_auth(mock_client, caplog, mock_cloud): """Test invalid auth detected by server.""" conn = iot.CloudIoT(mock_cloud) mock_client.receive.side_effect = \ - client_exceptions.WSServerHandshakeError(None, None, code=401) + client_exceptions.WSServerHandshakeError(None, None, status=401) yield from conn.connect() diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index d9c29cdae83..6a1d5a55c47 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -89,7 +89,7 @@ async def test_register_before_setup(hass): assert intent.text_input == 'I would like the Grolsch beer' -async def test_http_processing_intent(hass, test_client): +async def test_http_processing_intent(hass, aiohttp_client): """Test processing intent via HTTP API.""" class TestIntentHandler(intent.IntentHandler): """Test Intent Handler.""" @@ -119,7 +119,7 @@ async def test_http_processing_intent(hass, test_client): }) assert result - client = await test_client(hass.http.app) + client = await aiohttp_client(hass.http.app) resp = await client.post('/api/conversation/process', json={ 'text': 'I would like the Grolsch beer' }) @@ -243,7 +243,7 @@ async def test_toggle_intent(hass, sentence): assert call.data == {'entity_id': 'light.kitchen'} -async def test_http_api(hass, test_client): +async def test_http_api(hass, aiohttp_client): """Test the HTTP conversation API.""" result = await component.async_setup(hass, {}) assert result @@ -251,7 +251,7 @@ async def test_http_api(hass, test_client): result = await async_setup_component(hass, 'conversation', {}) assert result - client = await test_client(hass.http.app) + client = await aiohttp_client(hass.http.app) hass.states.async_set('light.kitchen', 'off') calls = async_mock_service(hass, 'homeassistant', 'turn_on') @@ -267,7 +267,7 @@ async def test_http_api(hass, test_client): assert call.data == {'entity_id': 'light.kitchen'} -async def test_http_api_wrong_data(hass, test_client): +async def test_http_api_wrong_data(hass, aiohttp_client): """Test the HTTP conversation API.""" result = await component.async_setup(hass, {}) assert result @@ -275,7 +275,7 @@ async def test_http_api_wrong_data(hass, test_client): result = await async_setup_component(hass, 'conversation', {}) assert result - client = await test_client(hass.http.app) + client = await aiohttp_client(hass.http.app) resp = await client.post('/api/conversation/process', json={ 'text': 123 From f1f4d80f24c9b3d4d8dbb775dc7b5427b404916f Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed, 30 May 2018 12:39:27 +0200 Subject: [PATCH 087/137] Homekit Bugfixes (#14689) * Fix async bug * Fix debounce bug --- .../components/homekit/accessories.py | 52 ++++++++----------- tests/components/homekit/test_accessories.py | 3 +- .../homekit/test_type_thermostats.py | 4 ++ 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 711bf0030f0..1b0d5ce1be4 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -1,6 +1,6 @@ """Extend the basic Accessory and Bridge functions.""" from datetime import timedelta -from functools import wraps +from functools import partial, wraps from inspect import getmodule import logging @@ -27,35 +27,25 @@ _LOGGER = logging.getLogger(__name__) def debounce(func): """Decorator function. Debounce callbacks form HomeKit.""" @ha_callback - def call_later_listener(*args): + def call_later_listener(self, *args): """Callback listener called from call_later.""" - # pylint: disable=unsubscriptable-object - nonlocal lastargs, remove_listener - hass = lastargs['hass'] - hass.async_add_job(func, *lastargs['args']) - lastargs = remove_listener = None + debounce_params = self.debounce.pop(func.__name__, None) + if debounce_params: + self.hass.async_add_job(func, self, *debounce_params[1:]) @wraps(func) - def wrapper(*args): - """Wrapper starts async timer. - - The accessory must have 'self.hass' and 'self.entity_id' as attributes. - """ - # pylint: disable=not-callable - hass = args[0].hass - nonlocal lastargs, remove_listener - if remove_listener: - remove_listener() - lastargs = remove_listener = None - lastargs = {'hass': hass, 'args': [*args]} + def wrapper(self, *args): + """Wrapper starts async timer.""" + debounce_params = self.debounce.pop(func.__name__, None) + if debounce_params: + debounce_params[0]() # remove listener remove_listener = track_point_in_utc_time( - hass, call_later_listener, + self.hass, partial(call_later_listener, self), dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT)) - logger.debug('%s: Start %s timeout', args[0].entity_id, + self.debounce[func.__name__] = (remove_listener, *args) + logger.debug('%s: Start %s timeout', self.entity_id, func.__name__.replace('set_', '')) - remove_listener = None - lastargs = None name = getmodule(func).__name__ logger = logging.getLogger(name) return wrapper @@ -76,11 +66,15 @@ class HomeAccessory(Accessory): self.config = config self.entity_id = entity_id self.hass = hass + self.debounce = {} - def run(self): - """Method called by accessory after driver is started.""" + async def run(self): + """Method called by accessory after driver is started. + + Run inside the HAP-python event loop. + """ state = self.hass.states.get(self.entity_id) - self.update_state_callback(new_state=state) + self.hass.add_job(self.update_state_callback, None, None, state) async_track_state_change( self.hass, self.entity_id, self.update_state_callback) @@ -127,10 +121,10 @@ class HomeDriver(AccessoryDriver): def pair(self, client_uuid, client_public): """Override super function to dismiss setup message if paired.""" - value = super().pair(client_uuid, client_public) - if value: + success = super().pair(client_uuid, client_public) + if success: dismiss_setup_message(self.hass) - return value + return success def unpair(self, client_uuid): """Override super function to show setup message if unpaired.""" diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index a0764d58000..711c38443f2 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -26,7 +26,7 @@ async def test_debounce(hass): arguments = None counter = 0 - mock = Mock(hass=hass) + mock = Mock(hass=hass, debounce={}) debounce_demo = debounce(demo_func) assert debounce_demo.__name__ == 'demo_func' @@ -76,6 +76,7 @@ async def test_home_accessory(hass, hk_driver): with patch('homeassistant.components.homekit.accessories.' 'HomeAccessory.update_state') as mock_update_state: await hass.async_add_job(acc.run) + await hass.async_block_till_done() state = hass.states.get(entity_id) mock_update_state.assert_called_with(state) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 6d6a48c7971..1f6554496a9 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -35,6 +35,7 @@ async def test_default_thermostat(hass, hk_driver, cls): await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() assert acc.aid == 2 assert acc.category == 9 # Thermostat @@ -175,6 +176,7 @@ async def test_auto_thermostat(hass, hk_driver, cls): await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() assert acc.char_cooling_thresh_temp.value == 23.0 assert acc.char_heating_thresh_temp.value == 19.0 @@ -254,6 +256,7 @@ async def test_power_state(hass, hk_driver, cls): await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() assert acc.support_power_state is True assert acc.char_current_heat_cool.value == 1 @@ -306,6 +309,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls): await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() hass.states.async_set(entity_id, STATE_AUTO, {ATTR_OPERATION_MODE: STATE_AUTO, From c14e41f4310a010633bc0eaad22a2a07e05adbdb Mon Sep 17 00:00:00 2001 From: Michael Nosthoff Date: Wed, 30 May 2018 16:53:35 +0200 Subject: [PATCH 088/137] Netatmo Sensor: Implement device_class (#14634) added device_class and removed icon for temperature and humidity. --- homeassistant/components/sensor/netatmo.py | 60 +++++++++++++--------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 4aeba082e55..f09e1d4f395 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -10,7 +10,9 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN +from homeassistant.const import ( + TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + STATE_UNKNOWN) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -26,28 +28,29 @@ DEPENDENCIES = ['netatmo'] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600) SENSOR_TYPES = { - 'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], - 'co2': ['CO2', 'ppm', 'mdi:cloud'], - 'pressure': ['Pressure', 'mbar', 'mdi:gauge'], - 'noise': ['Noise', 'dB', 'mdi:volume-high'], - 'humidity': ['Humidity', '%', 'mdi:water-percent'], - 'rain': ['Rain', 'mm', 'mdi:weather-rainy'], - 'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy'], - 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'], - 'battery_vp': ['Battery', '', 'mdi:battery'], - 'battery_lvl': ['Battery_lvl', '', 'mdi:battery'], - 'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer'], - 'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer'], - 'windangle': ['Angle', '', 'mdi:compass'], - 'windangle_value': ['Angle Value', 'º', 'mdi:compass'], - 'windstrength': ['Strength', 'km/h', 'mdi:weather-windy'], - 'gustangle': ['Gust Angle', '', 'mdi:compass'], - 'gustangle_value': ['Gust Angle Value', 'º', 'mdi:compass'], - 'guststrength': ['Gust Strength', 'km/h', 'mdi:weather-windy'], - 'rf_status': ['Radio', '', 'mdi:signal'], - 'rf_status_lvl': ['Radio_lvl', '', 'mdi:signal'], - 'wifi_status': ['Wifi', '', 'mdi:wifi'], - 'wifi_status_lvl': ['Wifi_lvl', 'dBm', 'mdi:wifi'] + 'temperature': ['Temperature', TEMP_CELSIUS, None, + DEVICE_CLASS_TEMPERATURE], + 'co2': ['CO2', 'ppm', 'mdi:cloud', None], + 'pressure': ['Pressure', 'mbar', 'mdi:gauge', None], + 'noise': ['Noise', 'dB', 'mdi:volume-high', None], + 'humidity': ['Humidity', '%', None, DEVICE_CLASS_HUMIDITY], + 'rain': ['Rain', 'mm', 'mdi:weather-rainy', None], + 'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy', None], + 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy', None], + 'battery_vp': ['Battery', '', 'mdi:battery', None], + 'battery_lvl': ['Battery_lvl', '', 'mdi:battery', None], + 'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer', None], + 'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer', None], + 'windangle': ['Angle', '', 'mdi:compass', None], + 'windangle_value': ['Angle Value', 'º', 'mdi:compass', None], + 'windstrength': ['Strength', 'km/h', 'mdi:weather-windy', None], + 'gustangle': ['Gust Angle', '', 'mdi:compass', None], + 'gustangle_value': ['Gust Angle Value', 'º', 'mdi:compass', None], + 'guststrength': ['Gust Strength', 'km/h', 'mdi:weather-windy', None], + 'rf_status': ['Radio', '', 'mdi:signal', None], + 'rf_status_lvl': ['Radio_lvl', '', 'mdi:signal', None], + 'wifi_status': ['Wifi', '', 'mdi:wifi', None], + 'wifi_status_lvl': ['Wifi_lvl', 'dBm', 'mdi:wifi', None] } MODULE_SCHEMA = vol.Schema({ @@ -106,7 +109,9 @@ class NetAtmoSensor(Entity): self.module_name = module_name self.type = sensor_type self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._device_class = SENSOR_TYPES[self.type][3] + self._icon = SENSOR_TYPES[self.type][2] + self._unit_of_measurement = SENSOR_TYPES[self.type][1] module_id = self.netatmo_data.\ station_data.moduleByName(module=module_name)['_id'] self.module_id = module_id[1] @@ -119,7 +124,12 @@ class NetAtmoSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return SENSOR_TYPES[self.type][2] + return self._icon + + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class @property def state(self): From 08fc73aa202f99c55025205b827efe765d0c9306 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 May 2018 11:19:27 -0400 Subject: [PATCH 089/137] Bump to 0.71.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index acc30bcd57c..4c9757b3260 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 = 70 +MINOR_VERSION = 71 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 7094d6d61e5d04b3b716ff7538561b2bc1bb9a06 Mon Sep 17 00:00:00 2001 From: c727 Date: Thu, 31 May 2018 14:31:40 +0200 Subject: [PATCH 090/137] Change ACP code_format to None|"Number"|"Any" (#14686) --- homeassistant/components/alarm_control_panel/alarmdecoder.py | 2 +- homeassistant/components/alarm_control_panel/alarmdotcom.py | 4 ++-- homeassistant/components/alarm_control_panel/concord232.py | 2 +- homeassistant/components/alarm_control_panel/envisalink.py | 2 +- homeassistant/components/alarm_control_panel/ifttt.py | 4 ++-- homeassistant/components/alarm_control_panel/manual.py | 4 ++-- homeassistant/components/alarm_control_panel/manual_mqtt.py | 4 ++-- homeassistant/components/alarm_control_panel/mqtt.py | 4 ++-- homeassistant/components/alarm_control_panel/nx584.py | 2 +- homeassistant/components/alarm_control_panel/satel_integra.py | 2 +- homeassistant/components/alarm_control_panel/simplisafe.py | 4 ++-- homeassistant/components/alarm_control_panel/verisure.py | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py index 13b51aea701..626022e362a 100644 --- a/homeassistant/components/alarm_control_panel/alarmdecoder.py +++ b/homeassistant/components/alarm_control_panel/alarmdecoder.py @@ -101,7 +101,7 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): """Return one or more digits/characters.""" - return '^\\d+$' + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 6b523e8b606..87e85f09da0 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -84,8 +84,8 @@ class AlarmDotCom(alarm.AlarmControlPanel): if self._code is None: return None elif isinstance(self._code, str) and re.search('^\\d+$', self._code): - return '^\\d+$' - return '.+' + return 'Number' + return 'Any' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py index bd3ee762ccb..9a65fdaff06 100644 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -80,7 +80,7 @@ class Concord232Alarm(alarm.AlarmControlPanel): @property def code_format(self): """Return the characters if code is defined.""" - return '^\\d+$' + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index 19bbfa611f2..25224484c79 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -106,7 +106,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Regex for code format or None if no code is required.""" if self._code: return None - return '^\\d+$' + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py index 203044f3915..209c5367c92 100644 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -129,8 +129,8 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel): if self._code is None: return None elif isinstance(self._code, str) and re.search('^\\d+$', self._code): - return '^\\d+$' - return '.+' + return 'Number' + return 'Any' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index e66251143da..2f2f89b9dfc 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -206,8 +206,8 @@ class ManualAlarm(alarm.AlarmControlPanel): if self._code is None: return None elif isinstance(self._code, str) and re.search('^\\d+$', self._code): - return '^\\d+$' - return '.+' + return 'Number' + return 'Any' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index c09105c91e0..895f5edd5da 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -242,8 +242,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): if self._code is None: return None elif isinstance(self._code, str) and re.search('^\\d+$', self._code): - return '^\\d+$' - return '.+' + return 'Number' + return 'Any' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 0298c7384a2..8a0dfefdc70 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -122,8 +122,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): if self._code is None: return None elif isinstance(self._code, str) and re.search('^\\d+$', self._code): - return '^\\d+$' - return '.+' + return 'Number' + return 'Any' @asyncio.coroutine def async_alarm_disarm(self, code=None): diff --git a/homeassistant/components/alarm_control_panel/nx584.py b/homeassistant/components/alarm_control_panel/nx584.py index 67d3725fc38..ca6f1a44a6f 100644 --- a/homeassistant/components/alarm_control_panel/nx584.py +++ b/homeassistant/components/alarm_control_panel/nx584.py @@ -70,7 +70,7 @@ class NX584Alarm(alarm.AlarmControlPanel): @property def code_format(self): """Return one or more digits/characters.""" - return '^\\d+$' + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/satel_integra.py b/homeassistant/components/alarm_control_panel/satel_integra.py index 964047f91e9..4ac3a93fff4 100644 --- a/homeassistant/components/alarm_control_panel/satel_integra.py +++ b/homeassistant/components/alarm_control_panel/satel_integra.py @@ -66,7 +66,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): """Return the regex for code format or None if no code is required.""" - return '^\\d{4,6}$' + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index c08ac3c0ea0..b4906acba3c 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -88,8 +88,8 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): if self._code is None: return None elif isinstance(self._code, str) and re.search('^\\d+$', self._code): - return '^\\d+$' - return '.+' + return 'Number' + return 'Any' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index 6651334400f..59bfe15fa9b 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -61,7 +61,7 @@ class VerisureAlarm(alarm.AlarmControlPanel): @property def code_format(self): """Return one or more digits/characters.""" - return '^\\d+$' + return 'Number' @property def changed_by(self): From 60f692c7bbd7282678a10ee6e9c4f02e2e174002 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 31 May 2018 10:55:50 -0600 Subject: [PATCH 091/137] Fixes (and stabilizes) some incorrect zone codes in RainMachine (#14719) * Fixes (and stabilizes) some incorrect zone codes * Fixed a misspelling --- .../components/switch/rainmachine.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index f4b2d780a9a..bdee64a3d54 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -83,7 +83,7 @@ SPRINKLER_TYPE_MAP = { 1: 'Popup Spray', 2: 'Rotors', 3: 'Surface Drip', - 4: 'Bubblers', + 4: 'Bubblers Drip', 99: 'Other' } @@ -96,14 +96,14 @@ SUN_EXPOSURE_MAP = { VEGETATION_MAP = { 0: 'Not Set', - 1: 'Not Set', - 2: 'Grass', + 2: 'Cool Season Grass', 3: 'Fruit Trees', 4: 'Flowers', 5: 'Vegetables', 6: 'Citrus', - 7: 'Bushes', - 8: 'Xeriscape', + 7: 'Trees and Bushes', + 9: 'Drought Tolerant Plants', + 10: 'Warm Season Grass', 99: 'Other' } @@ -296,15 +296,17 @@ class RainMachineZone(RainMachineSwitch): self._properties_json.get( 'waterSense').get('precipitationRate'), ATTR_RESTRICTIONS: self._obj.get('restriction'), - ATTR_SLOPE: SLOPE_TYPE_MAP[self._properties_json.get('slope')], + ATTR_SLOPE: SLOPE_TYPE_MAP.get( + self._properties_json.get('slope')), ATTR_SOIL_TYPE: - SOIL_TYPE_MAP[self._properties_json.get('sun')], + SOIL_TYPE_MAP.get(self._properties_json.get('sun')), ATTR_SPRINKLER_TYPE: - SPRINKLER_TYPE_MAP[self._properties_json.get('group_id')], + SPRINKLER_TYPE_MAP.get( + self._properties_json.get('group_id')), ATTR_SUN_EXPOSURE: - SUN_EXPOSURE_MAP[self._properties_json.get('sun')], + SUN_EXPOSURE_MAP.get(self._properties_json.get('sun')), ATTR_VEGETATION_TYPE: - VEGETATION_MAP[self._obj.get('type')], + VEGETATION_MAP.get(self._obj.get('type')), }) except RainMachineError as exc_info: _LOGGER.error('Unable to update info for zone "%s"', From dae90abb34586c989ca5ce0610226ec04e84965c Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Thu, 31 May 2018 10:34:07 -0600 Subject: [PATCH 092/137] Change climate default limits to constants Min and max temp and humidity are now defined in climate __init__.py and are available for import in subclasses. --- homeassistant/components/climate/__init__.py | 14 ++++++++++---- .../components/climate/generic_thermostat.py | 7 ++++--- homeassistant/components/climate/sensibo.py | 6 +++--- homeassistant/components/climate/tado.py | 7 ++++--- tests/components/climate/test_mqtt.py | 6 +++--- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 550d4035ddd..7f5ef4c4e80 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -22,6 +22,12 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS, ) + +DEFAULT_MIN_TEMP = 7 +DEFAULT_MAX_TEMP = 35 +DEFAULT_MIN_HUMITIDY = 30 +DEFAULT_MAX_HUMIDITY = 99 + DOMAIN = 'climate' ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -778,19 +784,19 @@ class ClimateDevice(Entity): @property def min_temp(self): """Return the minimum temperature.""" - return convert_temperature(7, TEMP_CELSIUS, self.temperature_unit) + return convert_temperature(DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit) @property def max_temp(self): """Return the maximum temperature.""" - return convert_temperature(35, TEMP_CELSIUS, self.temperature_unit) + return convert_temperature(DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit) @property def min_humidity(self): """Return the minimum humidity.""" - return 30 + return DEFAULT_MIN_HUMITIDY @property def max_humidity(self): """Return the maximum humidity.""" - return 99 + return DEFAULT_MAX_HUMIDITY diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index b5d3c3f7c25..ce8217aa92c 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -14,7 +14,8 @@ from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice, ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA) + SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA, + DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, @@ -268,7 +269,7 @@ class GenericThermostat(ClimateDevice): return self._min_temp # get default temp from super class - return ClimateDevice.min_temp.fget(self) + return DEFAULT_MIN_TEMP @property def max_temp(self): @@ -278,7 +279,7 @@ class GenericThermostat(ClimateDevice): return self._max_temp # Get default temp from super class - return ClimateDevice.max_temp.fget(self) + return DEFAULT_MAX_TEMP @asyncio.coroutine def _async_sensor_changed(self, entity_id, old_state, new_state): diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index 2b92d050d3b..94d9612755c 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -19,7 +19,7 @@ from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, - SUPPORT_ON_OFF) + SUPPORT_ON_OFF, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -240,13 +240,13 @@ class SensiboClimate(ClimateDevice): def min_temp(self): """Return the minimum temperature.""" return self._temperatures_list[0] \ - if self._temperatures_list else super().min_temp + if self._temperatures_list else DEFAULT_MIN_TEMP @property def max_temp(self): """Return the maximum temperature.""" return self._temperatures_list[-1] \ - if self._temperatures_list else super().max_temp + if self._temperatures_list else DEFAULT_MAX_TEMP @property def unique_id(self): diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py index 437c8ec3371..c3004a0407f 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -8,7 +8,8 @@ import logging from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS) from homeassistant.components.climate import ( - ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) + ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, + DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) from homeassistant.const import ATTR_TEMPERATURE from homeassistant.components.tado import DATA_TADO @@ -233,7 +234,7 @@ class TadoClimate(ClimateDevice): if self._min_temp: return self._min_temp # get default temp from super class - return super().min_temp + return DEFAULT_MIN_TEMP @property def max_temp(self): @@ -241,7 +242,7 @@ class TadoClimate(ClimateDevice): if self._max_temp: return self._max_temp # Get default temp from super class - return super().max_temp + return DEFAULT_MAX_TEMP def update(self): """Update the state of this climate device.""" diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 663393503ac..677d1b944d0 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -9,9 +9,9 @@ from homeassistant.setup import setup_component from homeassistant.components import climate from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.climate import ( - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, - SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, + SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP) from tests.common import (get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, mock_component) From cc264f415e746173aff8cbb182b8ddc81ddebb8d Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Thu, 31 May 2018 11:32:31 -0600 Subject: [PATCH 093/137] Fix PEP-8 issues --- homeassistant/components/climate/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 7f5ef4c4e80..ebe7cbbf2c1 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -784,12 +784,14 @@ class ClimateDevice(Entity): @property def min_temp(self): """Return the minimum temperature.""" - return convert_temperature(DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit) + return convert_temperature(DEFAULT_MIN_TEMP, TEMP_CELSIUS, + self.temperature_unit) @property def max_temp(self): """Return the maximum temperature.""" - return convert_temperature(DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit) + return convert_temperature(DEFAULT_MAX_TEMP, TEMP_CELSIUS, + self.temperature_unit) @property def min_humidity(self): From 753fe8279ba019d376eeb1caafcd8640b4d29f98 Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Thu, 31 May 2018 13:59:26 -0600 Subject: [PATCH 094/137] Remove deprecated comments --- homeassistant/components/climate/generic_thermostat.py | 2 -- homeassistant/components/climate/tado.py | 4 ++-- tests/components/climate/test_mqtt.py | 6 +++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index ce8217aa92c..6b7f6cb2afc 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -268,7 +268,6 @@ class GenericThermostat(ClimateDevice): if self._min_temp: return self._min_temp - # get default temp from super class return DEFAULT_MIN_TEMP @property @@ -278,7 +277,6 @@ class GenericThermostat(ClimateDevice): if self._max_temp: return self._max_temp - # Get default temp from super class return DEFAULT_MAX_TEMP @asyncio.coroutine diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py index c3004a0407f..59da425553a 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -233,7 +233,7 @@ class TadoClimate(ClimateDevice): """Return the minimum temperature.""" if self._min_temp: return self._min_temp - # get default temp from super class + return DEFAULT_MIN_TEMP @property @@ -241,7 +241,7 @@ class TadoClimate(ClimateDevice): """Return the maximum temperature.""" if self._max_temp: return self._max_temp - # Get default temp from super class + return DEFAULT_MAX_TEMP def update(self): diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 677d1b944d0..663393503ac 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -9,9 +9,9 @@ from homeassistant.setup import setup_component from homeassistant.components import climate from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.climate import ( - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, - SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, + SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT) from tests.common import (get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, mock_component) From 14ee6178f9e3c3d40f243edc27c2c0dc4376f4a3 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 31 May 2018 23:07:50 +0200 Subject: [PATCH 095/137] Add Flock notification platform (#14533) * Add Flock notification platform * Use async syntax and move session and loop --- .coveragerc | 3 +- homeassistant/components/notify/flock.py | 61 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/notify/flock.py diff --git a/.coveragerc b/.coveragerc index 8d884dc53e6..26744ad6952 100644 --- a/.coveragerc +++ b/.coveragerc @@ -523,9 +523,10 @@ omit = homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/ciscospark.py homeassistant/components/notify/clickatell.py - homeassistant/components/notify/clicksend_tts.py homeassistant/components/notify/clicksend.py + homeassistant/components/notify/clicksend_tts.py homeassistant/components/notify/discord.py + homeassistant/components/notify/flock.py homeassistant/components/notify/free_mobile.py homeassistant/components/notify/gntp.py homeassistant/components/notify/group.py diff --git a/homeassistant/components/notify/flock.py b/homeassistant/components/notify/flock.py new file mode 100644 index 00000000000..d26f629809f --- /dev/null +++ b/homeassistant/components/notify/flock.py @@ -0,0 +1,61 @@ +""" +Flock platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.flock/ +""" +import asyncio +import logging + +import async_timeout +import voluptuous as vol + +from homeassistant.components.notify import ( + PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) +_RESOURCE = 'https://api.flock.com/hooks/sendMessage/' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, +}) + + +async def get_service(hass, config, discovery_info=None): + """Get the Flock notification service.""" + access_token = config.get(CONF_ACCESS_TOKEN) + url = '{}{}'.format(_RESOURCE, access_token) + session = async_get_clientsession(hass) + + return FlockNotificationService(url, session, hass.loop) + + +class FlockNotificationService(BaseNotificationService): + """Implement the notification service for Flock.""" + + def __init__(self, url, session, loop): + """Initialize the Flock notification service.""" + self._loop = loop + self._url = url + self._session = session + + async def async_send_message(self, message, **kwargs): + """Send the message to the user.""" + payload = {'text': message} + + _LOGGER.debug("Attempting to call Flock at %s", self._url) + + try: + with async_timeout.timeout(10, loop=self._loop): + response = await self._session.post(self._url, json=payload) + result = await response.json() + + if response.status != 200 or 'error' in result: + _LOGGER.error( + "Flock service returned HTTP status %d, response %s", + response.status, result) + except asyncio.TimeoutError: + _LOGGER.error("Timeout accessing Flock at %s", self._url) From a58a566ae842a2b68c045ad60411a8be5085c673 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 May 2018 17:25:35 -0400 Subject: [PATCH 096/137] Bump frontend to 20180531.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 2bd7283e38e..5ebf6e8762f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.4'] +REQUIREMENTS = ['home-assistant-frontend==20180531.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 3a190a29ed8..6354e9c49c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.4 +home-assistant-frontend==20180531.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56b7eec6c5e..00b3d1f82e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.4 +home-assistant-frontend==20180531.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From f32b50cb809ad91f09eae78f75d5db9e4e711b53 Mon Sep 17 00:00:00 2001 From: Marius Date: Fri, 1 Jun 2018 00:26:59 +0300 Subject: [PATCH 097/137] Fix Eco mode display on Nest (#14706) * Fix Eco mode display on Nest * Fix Hound problems --- homeassistant/components/climate/nest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 0a5344fdf98..28e8020ab90 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -134,7 +134,9 @@ class NestThermostat(ClimateDevice): @property def target_temperature(self): """Return the temperature we try to reach.""" - if self._mode != NEST_MODE_HEAT_COOL and not self.is_away_mode_on: + if self._mode != NEST_MODE_HEAT_COOL and \ + self._mode != STATE_ECO and \ + not self.is_away_mode_on: return self._target_temperature return None From ed9cf994c202ef3b051e110bed60fbe511239d06 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 May 2018 17:58:03 -0400 Subject: [PATCH 098/137] Revert "Remove simplepush.io (#14358)" This reverts commit 612a37b2dd37f4856ac7103bb7bc6f7dc6d8b970. --- .coveragerc | 1 + homeassistant/components/notify/simplepush.py | 59 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 63 insertions(+) create mode 100644 homeassistant/components/notify/simplepush.py diff --git a/.coveragerc b/.coveragerc index 26744ad6952..dfbbb232efc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -547,6 +547,7 @@ omit = homeassistant/components/notify/rest.py homeassistant/components/notify/rocketchat.py homeassistant/components/notify/sendgrid.py + homeassistant/components/notify/simplepush.py homeassistant/components/notify/slack.py homeassistant/components/notify/smtp.py homeassistant/components/notify/stride.py diff --git a/homeassistant/components/notify/simplepush.py b/homeassistant/components/notify/simplepush.py new file mode 100644 index 00000000000..9d5c58fc5b1 --- /dev/null +++ b/homeassistant/components/notify/simplepush.py @@ -0,0 +1,59 @@ +""" +Simplepush notification service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.simplepush/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_PASSWORD + +REQUIREMENTS = ['simplepush==1.1.4'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_ENCRYPTED = 'encrypted' + +CONF_DEVICE_KEY = 'device_key' +CONF_EVENT = 'event' +CONF_SALT = 'salt' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICE_KEY): cv.string, + vol.Optional(CONF_EVENT): cv.string, + vol.Inclusive(CONF_PASSWORD, ATTR_ENCRYPTED): cv.string, + vol.Inclusive(CONF_SALT, ATTR_ENCRYPTED): cv.string, +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Simplepush notification service.""" + return SimplePushNotificationService(config) + + +class SimplePushNotificationService(BaseNotificationService): + """Implementation of the notification service for Simplepush.""" + + def __init__(self, config): + """Initialize the Simplepush notification service.""" + self._device_key = config.get(CONF_DEVICE_KEY) + self._event = config.get(CONF_EVENT) + self._password = config.get(CONF_PASSWORD) + self._salt = config.get(CONF_SALT) + + def send_message(self, message='', **kwargs): + """Send a message to a Simplepush user.""" + from simplepush import send, send_encrypted + + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + + if self._password: + send_encrypted(self._device_key, self._password, self._salt, title, + message, event=self._event) + else: + send(self._device_key, title, message, event=self._event) diff --git a/requirements_all.txt b/requirements_all.txt index 6354e9c49c7..a901c9cb153 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1204,6 +1204,9 @@ sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan shodan==1.7.7 +# homeassistant.components.notify.simplepush +simplepush==1.1.4 + # homeassistant.components.alarm_control_panel.simplisafe simplisafe-python==1.0.5 From 99fdd3e358ad0fa2d04c3703eaf2be4af1ffd29a Mon Sep 17 00:00:00 2001 From: glenn20 Date: Fri, 1 Jun 2018 08:32:09 +1000 Subject: [PATCH 099/137] Add device_descriptor and device_name to keyboard event (#14642) * Add device_descriptor and device_name to keyboard event This allows automations to identify which device has generated the keypress. This is especially useful for bluetooth remotes to control different devices. * Remove line breaks * Fix --- homeassistant/components/keyboard_remote.py | 49 +++++++-------------- 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote.py index d737c555873..af45bd3d4f9 100644 --- a/homeassistant/components/keyboard_remote.py +++ b/homeassistant/components/keyboard_remote.py @@ -50,10 +50,7 @@ def setup(hass, config): """Set up the keyboard_remote.""" config = config.get(DOMAIN) - keyboard_remote = KeyboardRemote( - hass, - config - ) + keyboard_remote = KeyboardRemote(hass, config) def _start_keyboard_remote(_event): keyboard_remote.run() @@ -61,14 +58,8 @@ def setup(hass, config): def _stop_keyboard_remote(_event): keyboard_remote.stop() - hass.bus.listen_once( - EVENT_HOMEASSISTANT_START, - _start_keyboard_remote - ) - hass.bus.listen_once( - EVENT_HOMEASSISTANT_STOP, - _stop_keyboard_remote - ) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_keyboard_remote) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_keyboard_remote) return True @@ -93,10 +84,8 @@ class KeyboardRemoteThread(threading.Thread): _LOGGER.debug("Keyboard connected, %s", self.device_id) else: _LOGGER.debug( - 'Keyboard not connected, %s.\n\ - Check /dev/input/event* permissions.', - self.device_id - ) + "Keyboard not connected, %s. " + "Check /dev/input/event* permissions", self.device_id) id_folder = '/dev/input/by-id/' @@ -105,12 +94,9 @@ class KeyboardRemoteThread(threading.Thread): device_names = [InputDevice(file_name).name for file_name in list_devices()] _LOGGER.debug( - 'Possible device names are:\n %s.\n \ - Possible device descriptors are %s:\n %s', - device_names, - id_folder, - os.listdir(id_folder) - ) + "Possible device names are: %s. " + "Possible device descriptors are %s: %s", + device_names, id_folder, os.listdir(id_folder)) threading.Thread.__init__(self) self.stopped = threading.Event() @@ -149,9 +135,7 @@ class KeyboardRemoteThread(threading.Thread): self.dev = self._get_keyboard_device() if self.dev is not None: self.dev.grab() - self.hass.bus.fire( - KEYBOARD_REMOTE_CONNECTED - ) + self.hass.bus.fire(KEYBOARD_REMOTE_CONNECTED) _LOGGER.debug("Keyboard re-connected, %s", self.device_id) else: continue @@ -160,9 +144,7 @@ class KeyboardRemoteThread(threading.Thread): event = self.dev.read_one() except IOError: # Keyboard Disconnected self.dev = None - self.hass.bus.fire( - KEYBOARD_REMOTE_DISCONNECTED - ) + self.hass.bus.fire(KEYBOARD_REMOTE_DISCONNECTED) _LOGGER.debug("Keyboard disconnected, %s", self.device_id) continue @@ -174,7 +156,11 @@ class KeyboardRemoteThread(threading.Thread): _LOGGER.debug(categorize(event)) self.hass.bus.fire( KEYBOARD_REMOTE_COMMAND_RECEIVED, - {KEY_CODE: event.code} + { + KEY_CODE: event.code, + DEVICE_DESCRIPTOR: self.device_descriptor, + DEVICE_NAME: self.device_name + } ) @@ -191,9 +177,8 @@ class KeyboardRemote(object): if device_descriptor is not None\ or device_name is not None: - thread = KeyboardRemoteThread(hass, device_name, - device_descriptor, - key_value) + thread = KeyboardRemoteThread( + hass, device_name, device_descriptor, key_value) self.threads.append(thread) def run(self): From de56a0d021bb803f2a980bdace6d614c8f28045f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jun 2018 08:40:27 +0200 Subject: [PATCH 100/137] Upgrade shodan to 1.8.0 (#14717) --- homeassistant/components/sensor/shodan.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py index 720158e1029..1cc2ba30866 100644 --- a/homeassistant/components/sensor/shodan.py +++ b/homeassistant/components/sensor/shodan.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['shodan==1.7.7'] +REQUIREMENTS = ['shodan==1.8.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a901c9cb153..60b010ab306 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1202,7 +1202,7 @@ sense_energy==0.3.1 sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan -shodan==1.7.7 +shodan==1.8.0 # homeassistant.components.notify.simplepush simplepush==1.1.4 From 6cd69b413cfd6d4cd6ac3f53c4af82dd902f1e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Fri, 1 Jun 2018 02:41:40 -0400 Subject: [PATCH 101/137] Bump pyatv to 0.3.10 (#14736) * Bump pyatv to 0.3.10 * Update requirements_all.txt --- homeassistant/components/apple_tv.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index a9bd5c9c8bc..68445092db7 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -17,7 +17,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyatv==0.3.9'] +REQUIREMENTS = ['pyatv==0.3.10'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 60b010ab306..1c2f927eaf5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -731,7 +731,7 @@ pyasn1-modules==0.1.5 pyasn1==0.3.7 # homeassistant.components.apple_tv -pyatv==0.3.9 +pyatv==0.3.10 # homeassistant.components.device_tracker.bbox # homeassistant.components.sensor.bbox From ab3717af767d3c740b3e9567299afd363ccaf607 Mon Sep 17 00:00:00 2001 From: roiff Date: Fri, 1 Jun 2018 19:49:16 +0800 Subject: [PATCH 102/137] Homekit Thermostat: Better support for temperature ranges (#14679) * Support for obtaining temperature range * Fallback to Defaults * Fixed unit conversion * Added test --- .../components/homekit/type_thermostats.py | 39 +++++++++++---- .../homekit/test_type_thermostats.py | 48 +++++++++++++++++-- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index d6555d5056d..73a29990fba 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -4,15 +4,16 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, + ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, + ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, - TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES from .accessories import debounce, HomeAccessory @@ -20,7 +21,7 @@ from .const import ( CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE, - CHAR_TEMP_DISPLAY_UNITS, SERV_THERMOSTAT) + CHAR_TEMP_DISPLAY_UNITS, PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_THERMOSTAT) from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -42,17 +43,18 @@ class Thermostat(HomeAccessory): def __init__(self, *args): """Initialize a Thermostat accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) - self._unit = TEMP_CELSIUS + self._unit = self.hass.config.units.temperature_unit self.support_power_state = False self.heat_cool_flag_target_state = False self.temperature_flag_target_state = False self.coolingthresh_flag_target_state = False self.heatingthresh_flag_target_state = False + min_temp, max_temp = self.get_temperature_range() # Add additional characteristics if auto mode is supported self.chars = [] features = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_SUPPORTED_FEATURES) + .attributes.get(ATTR_SUPPORTED_FEATURES, 0) if features & SUPPORT_ON_OFF: self.support_power_state = True if features & SUPPORT_TEMP_RANGE: @@ -73,6 +75,8 @@ class Thermostat(HomeAccessory): CHAR_CURRENT_TEMPERATURE, value=21.0) self.char_target_temp = serv_thermostat.configure_char( CHAR_TARGET_TEMPERATURE, value=21.0, + properties={PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp}, setter_callback=self.set_target_temperature) # Display units characteristic @@ -85,12 +89,30 @@ class Thermostat(HomeAccessory): if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars: self.char_cooling_thresh_temp = serv_thermostat.configure_char( CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0, + properties={PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp}, setter_callback=self.set_cooling_threshold) if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars: self.char_heating_thresh_temp = serv_thermostat.configure_char( CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0, + properties={PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp}, setter_callback=self.set_heating_threshold) + def get_temperature_range(self): + """Return min and max temperature range.""" + max_temp = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MAX_TEMP) + max_temp = temperature_to_homekit(max_temp, self._unit) if max_temp \ + else DEFAULT_MAX_TEMP + + min_temp = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MIN_TEMP) + min_temp = temperature_to_homekit(min_temp, self._unit) if min_temp \ + else DEFAULT_MIN_TEMP + + return min_temp, max_temp + def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" if value in HC_HOMEKIT_TO_HASS: @@ -147,9 +169,6 @@ class Thermostat(HomeAccessory): def update_state(self, new_state): """Update security state after state changed.""" - 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)): diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 1f6554496a9..00e3e2d22fc 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,12 +1,16 @@ """Test different accessory types: Thermostats.""" from collections import namedtuple +from unittest.mock import patch import pytest from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, - ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, - DOMAIN, STATE_AUTO, STATE_COOL, STATE_HEAT) + ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TEMPERATURE, + ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, + ATTR_OPERATION_LIST, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, + STATE_AUTO, STATE_COOL, STATE_HEAT) +from homeassistant.components.homekit.const import ( + PROP_MAX_VALUE, PROP_MIN_VALUE) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -31,7 +35,7 @@ async def test_default_thermostat(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' - hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) + hass.states.async_set(entity_id, STATE_OFF) await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -48,6 +52,9 @@ async def test_default_thermostat(hass, hk_driver, cls): assert acc.char_cooling_thresh_temp is None assert acc.char_heating_thresh_temp is None + assert acc.char_target_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP + assert acc.char_target_temp.properties[PROP_MIN_VALUE] == DEFAULT_MIN_TEMP + hass.states.async_set(entity_id, STATE_HEAT, {ATTR_OPERATION_MODE: STATE_HEAT, ATTR_TEMPERATURE: 22.0, @@ -181,6 +188,15 @@ async def test_auto_thermostat(hass, hk_driver, cls): assert acc.char_cooling_thresh_temp.value == 23.0 assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] \ + == DEFAULT_MAX_TEMP + assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] \ + == DEFAULT_MIN_TEMP + assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] \ + == DEFAULT_MAX_TEMP + assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] \ + == DEFAULT_MIN_TEMP + hass.states.async_set(entity_id, STATE_AUTO, {ATTR_OPERATION_MODE: STATE_AUTO, ATTR_TARGET_TEMP_HIGH: 22.0, @@ -307,7 +323,9 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls): # support_auto = True hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) + with patch.object(hass.config.units, 'temperature_unit', + new=TEMP_FAHRENHEIT): + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) await hass.async_block_till_done() @@ -349,3 +367,23 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls): assert call_set_temperature[2] assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2 + + +async def test_get_temperature_range(hass, hk_driver, cls): + """Test if temperature range is evaluated correctly.""" + entity_id = 'climate.test' + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) + + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25}) + await hass.async_block_till_done() + assert acc.get_temperature_range() == (20, 25) + + acc._unit = TEMP_FAHRENHEIT + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70}) + await hass.async_block_till_done() + assert acc.get_temperature_range() == (15.6, 21.1) From f6eb9e79d5c4ead4d81ea04aafb165c7027fadab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jun 2018 10:06:17 -0400 Subject: [PATCH 103/137] Custom panel (#14708) * Add support for custom panels in JS * Allow specifying JS custom panels * Add trust external option * Fix tests * Do I/O outside event loop * Change config to avoid breaking change --- homeassistant/components/panel_custom.py | 49 ++++++++++---- tests/components/test_panel_custom.py | 83 +++++++++++++++++------- 2 files changed, 99 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/panel_custom.py b/homeassistant/components/panel_custom.py index 473d44f3b55..4659578ae27 100644 --- a/homeassistant/components/panel_custom.py +++ b/homeassistant/components/panel_custom.py @@ -4,7 +4,6 @@ Register a custom front end panel. For more details about this component, please refer to the documentation at https://home-assistant.io/components/panel_custom/ """ -import asyncio import logging import os @@ -21,27 +20,33 @@ CONF_SIDEBAR_ICON = 'sidebar_icon' CONF_URL_PATH = 'url_path' CONF_CONFIG = 'config' CONF_WEBCOMPONENT_PATH = 'webcomponent_path' +CONF_JS_URL = 'js_url' +CONF_EMBED_IFRAME = 'embed_iframe' +CONF_TRUST_EXTERNAL_SCRIPT = 'trust_external_script' DEFAULT_ICON = 'mdi:bookmark' +LEGACY_URL = '/api/panel_custom/{}' PANEL_DIR = 'panels' CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [{ - vol.Required(CONF_COMPONENT_NAME): cv.slug, + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_COMPONENT_NAME): cv.string, vol.Optional(CONF_SIDEBAR_TITLE): cv.string, vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon, vol.Optional(CONF_URL_PATH): cv.string, - vol.Optional(CONF_CONFIG): cv.match_all, + vol.Optional(CONF_CONFIG): dict, vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile, - }]) + vol.Optional(CONF_JS_URL): cv.string, + vol.Optional(CONF_EMBED_IFRAME, default=False): cv.boolean, + vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT, default=False): cv.boolean, + })]) }, extra=vol.ALLOW_EXTRA) _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize custom panel.""" success = False @@ -52,17 +57,39 @@ def async_setup(hass, config): if panel_path is None: panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name)) - if not os.path.isfile(panel_path): + custom_panel_config = { + 'name': name, + 'embed_iframe': panel[CONF_EMBED_IFRAME], + 'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT], + } + + if CONF_JS_URL in panel: + custom_panel_config['js_url'] = panel[CONF_JS_URL] + + elif not await hass.async_add_job(os.path.isfile, panel_path): _LOGGER.error('Unable to find webcomponent for %s: %s', name, panel_path) continue - yield from hass.components.frontend.async_register_panel( - name, panel_path, + else: + url = LEGACY_URL.format(name) + hass.http.register_static_path(url, panel_path) + custom_panel_config['html_url'] = LEGACY_URL.format(name) + + if CONF_CONFIG in panel: + # Make copy because we're mutating it + config = dict(panel[CONF_CONFIG]) + else: + config = {} + + config['_panel_custom'] = custom_panel_config + + await hass.components.frontend.async_register_built_in_panel( + component_name='custom', sidebar_title=panel.get(CONF_SIDEBAR_TITLE), sidebar_icon=panel.get(CONF_SIDEBAR_ICON), frontend_url_path=panel.get(CONF_URL_PATH), - config=panel.get(CONF_CONFIG), + config=config ) success = True diff --git a/tests/components/test_panel_custom.py b/tests/components/test_panel_custom.py index d33221da2a7..596aa1b3c0b 100644 --- a/tests/components/test_panel_custom.py +++ b/tests/components/test_panel_custom.py @@ -1,23 +1,11 @@ """The tests for the panel_custom component.""" -import asyncio from unittest.mock import Mock, patch -import pytest - from homeassistant import setup from homeassistant.components import frontend -from tests.common import mock_component - -@pytest.fixture(autouse=True) -def mock_frontend_loaded(hass): - """Mock frontend is loaded.""" - mock_component(hass, 'frontend') - - -@asyncio.coroutine -def test_webcomponent_custom_path_not_found(hass): +async def test_webcomponent_custom_path_not_found(hass): """Test if a web component is found in config panels dir.""" filename = 'mock.file' @@ -33,45 +21,96 @@ def test_webcomponent_custom_path_not_found(hass): } with patch('os.path.isfile', Mock(return_value=False)): - result = yield from setup.async_setup_component( + result = await setup.async_setup_component( hass, 'panel_custom', config ) assert not result assert len(hass.data.get(frontend.DATA_PANELS, {})) == 0 -@asyncio.coroutine -def test_webcomponent_custom_path(hass): +async def test_webcomponent_custom_path(hass): """Test if a web component is found in config panels dir.""" filename = 'mock.file' config = { 'panel_custom': { - 'name': 'todomvc', + 'name': 'todo-mvc', 'webcomponent_path': filename, 'sidebar_title': 'Sidebar Title', 'sidebar_icon': 'mdi:iconicon', 'url_path': 'nice_url', - 'config': 5, + 'config': { + 'hello': 'world', + } } } with patch('os.path.isfile', Mock(return_value=True)): with patch('os.access', Mock(return_value=True)): - result = yield from setup.async_setup_component( + result = await setup.async_setup_component( hass, 'panel_custom', config ) assert result panels = hass.data.get(frontend.DATA_PANELS, []) - assert len(panels) == 1 + assert panels assert 'nice_url' in panels panel = panels['nice_url'] - assert panel.config == 5 + assert panel.config == { + 'hello': 'world', + '_panel_custom': { + 'html_url': '/api/panel_custom/todo-mvc', + 'name': 'todo-mvc', + 'embed_iframe': False, + 'trust_external': False, + }, + } assert panel.frontend_url_path == 'nice_url' assert panel.sidebar_icon == 'mdi:iconicon' assert panel.sidebar_title == 'Sidebar Title' - assert panel.path == filename + + +async def test_js_webcomponent(hass): + """Test if a web component is found in config panels dir.""" + config = { + 'panel_custom': { + 'name': 'todo-mvc', + 'js_url': '/local/bla.js', + 'sidebar_title': 'Sidebar Title', + 'sidebar_icon': 'mdi:iconicon', + 'url_path': 'nice_url', + 'config': { + 'hello': 'world', + }, + 'embed_iframe': True, + 'trust_external_script': True, + } + } + + result = await setup.async_setup_component( + hass, 'panel_custom', config + ) + assert result + + panels = hass.data.get(frontend.DATA_PANELS, []) + + assert panels + assert 'nice_url' in panels + + panel = panels['nice_url'] + + assert panel.config == { + 'hello': 'world', + '_panel_custom': { + 'js_url': '/local/bla.js', + 'name': 'todo-mvc', + 'embed_iframe': True, + 'trust_external': True, + } + } + assert panel.frontend_url_path == 'nice_url' + assert panel.sidebar_icon == 'mdi:iconicon' + assert panel.sidebar_title == 'Sidebar Title' From fcbc399809e16dd9d79c1982c9ea16381cd4f88c Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 1 Jun 2018 16:27:12 +0200 Subject: [PATCH 104/137] Disallow automation.trigger without entity_id (#14724) --- homeassistant/components/automation/__init__.py | 2 +- tests/components/automation/test_init.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 2f510fd33d6..2a7a3887b34 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -98,7 +98,7 @@ SERVICE_SCHEMA = vol.Schema({ }) TRIGGER_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_VARIABLES, default={}): dict, }) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 7a8c097a730..33f1a7aa704 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -207,6 +207,7 @@ class TestAutomation(unittest.TestCase): """Test triggers.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { + 'alias': 'test', 'trigger': [ { 'platform': 'event', @@ -228,7 +229,9 @@ class TestAutomation(unittest.TestCase): self.hass.block_till_done() assert len(self.calls) == 0 - self.hass.services.call('automation', 'trigger', blocking=True) + self.hass.services.call('automation', 'trigger', + {'entity_id': 'automation.test'}, + blocking=True) self.hass.block_till_done() assert len(self.calls) == 1 From cba8333a1389410fce11aaf8afbdf3e358a6a871 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 1 Jun 2018 07:44:58 -0700 Subject: [PATCH 105/137] Change nest to cloud push (#14656) * Change nest component to Cloud Push Change sensors.nest, binary_sensors.nest and climate.nest to push mode nest camera still need poll to update snapshot image Also change nest component to async * Flake8 lint * Fix async_notify_errors, it is not a coroutine * Fix pylint * Fix pylint, function name should shall shorter than 32 * Use dispatcher helper instead event bus * Use async_update_ha_state(True) * Refactoring load_platform Move service registration into async_setup_nest(), resolve an issue that before the first time configuration done, set_mode service should not be registered * Fix an issue that authorization failure may leave a blocked thread * Pylinting * async_nest_update_callback => async_update_state to avoid confusion * Move signal handler register to async_added_to_hass * Better handle nest api error * Remove unnecessary register for binary_sensor * Remove unused import * Upgrade to python-nest 4.0.1 Fix a thread race condition issue * Address my own comments * Address my own comment --- .../components/binary_sensor/nest.py | 2 +- homeassistant/components/climate/nest.py | 40 +++-- homeassistant/components/nest.py | 150 ++++++++++++------ homeassistant/components/sensor/nest.py | 18 ++- requirements_all.txt | 2 +- 5 files changed, 149 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 2a1732cd9f0..008b6eed1e4 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -8,9 +8,9 @@ from itertools import chain import logging from homeassistant.components.binary_sensor import (BinarySensorDevice) +from homeassistant.components.nest import DATA_NEST from homeassistant.components.sensor.nest import NestSensor from homeassistant.const import CONF_MONITORED_CONDITIONS -from homeassistant.components.nest import DATA_NEST DEPENDENCIES = ['nest'] diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 28e8020ab90..696f1479c08 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -from homeassistant.components.nest import DATA_NEST +from homeassistant.components.nest import DATA_NEST, SIGNAL_NEST_UPDATE from homeassistant.components.climate import ( STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -18,6 +18,7 @@ from homeassistant.components.climate import ( from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN) +from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['nest'] _LOGGER = logging.getLogger(__name__) @@ -37,11 +38,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): temp_unit = hass.config.units.temperature_unit - add_devices( - [NestThermostat(structure, device, temp_unit) - for structure, device in hass.data[DATA_NEST].thermostats()], - True - ) + all_devices = [NestThermostat(structure, device, temp_unit) + for structure, device in hass.data[DATA_NEST].thermostats()] + + add_devices(all_devices, True) class NestThermostat(ClimateDevice): @@ -97,6 +97,20 @@ class NestThermostat(ClimateDevice): self._min_temperature = None self._max_temperature = None + @property + def should_poll(self): + """Do not need poll thanks using Nest streaming API.""" + return False + + async def async_added_to_hass(self): + """Register update signal handler.""" + async def async_update_state(): + """Update device state.""" + await self.async_update_ha_state(True) + + async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, + async_update_state) + @property def supported_features(self): """Return the list of supported features.""" @@ -170,18 +184,24 @@ class NestThermostat(ClimateDevice): def set_temperature(self, **kwargs): """Set new target temperature.""" import nest + temp = None target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) if self._mode == NEST_MODE_HEAT_COOL: if target_temp_low is not None and target_temp_high is not None: temp = (target_temp_low, target_temp_high) + _LOGGER.debug("Nest set_temperature-output-value=%s", temp) else: temp = kwargs.get(ATTR_TEMPERATURE) - _LOGGER.debug("Nest set_temperature-output-value=%s", temp) + _LOGGER.debug("Nest set_temperature-output-value=%s", temp) try: - self.device.target = temp - except nest.nest.APIError: - _LOGGER.error("An error occurred while setting the temperature") + if temp is not None: + self.device.target = temp + except nest.nest.APIError as api_error: + _LOGGER.error("An error occurred while setting temperature: %s", + api_error) + # restore target temperature + self.schedule_update_ha_state(True) def set_operation_mode(self, operation_mode): """Set operation mode.""" diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index f474bfa7a26..365f0593c8d 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -4,18 +4,20 @@ Support for Nest devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/nest/ """ +from concurrent.futures import ThreadPoolExecutor import logging import socket import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery from homeassistant.const import ( CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, - CONF_MONITORED_CONDITIONS) + CONF_MONITORED_CONDITIONS, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import discovery, config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send -REQUIREMENTS = ['python-nest==4.0.0'] +REQUIREMENTS = ['python-nest==4.0.1'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -24,6 +26,8 @@ DOMAIN = 'nest' DATA_NEST = 'nest' +SIGNAL_NEST_UPDATE = 'nest_update' + NEST_CONFIG_FILE = 'nest.conf' CONF_CLIENT_ID = 'client_id' CONF_CLIENT_SECRET = 'client_secret' @@ -51,23 +55,44 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -def request_configuration(nest, hass, config): +async def async_nest_update_event_broker(hass, nest): + """ + Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data. + + nest.update_event.wait will block the thread in most of time, + so specific an executor to save default thread pool. + """ + _LOGGER.debug("listening nest.update_event") + with ThreadPoolExecutor(max_workers=1) as executor: + while True: + await hass.loop.run_in_executor(executor, nest.update_event.wait) + if hass.is_running: + nest.update_event.clear() + _LOGGER.debug("dispatching nest data update") + async_dispatcher_send(hass, SIGNAL_NEST_UPDATE) + else: + return + + +async def async_request_configuration(nest, hass, config): """Request configuration steps from the user.""" configurator = hass.components.configurator if 'nest' in _CONFIGURING: _LOGGER.debug("configurator failed") - configurator.notify_errors( + configurator.async_notify_errors( _CONFIGURING['nest'], "Failed to configure, please try again.") return - def nest_configuration_callback(data): + async def async_nest_config_callback(data): """Run when the configuration callback is called.""" _LOGGER.debug("configurator callback") pin = data.get('pin') - setup_nest(hass, nest, config, pin=pin) + if await async_setup_nest(hass, nest, config, pin=pin): + # start nest update event listener as we missed startup hook + hass.async_add_job(async_nest_update_event_broker, hass, nest) - _CONFIGURING['nest'] = configurator.request_config( - "Nest", nest_configuration_callback, + _CONFIGURING['nest'] = configurator.async_request_config( + "Nest", async_nest_config_callback, description=('To configure Nest, click Request Authorization below, ' 'log into your Nest account, ' 'and then enter the resulting PIN'), @@ -78,60 +103,47 @@ def request_configuration(nest, hass, config): ) -def setup_nest(hass, nest, config, pin=None): +async def async_setup_nest(hass, nest, config, pin=None): """Set up the Nest devices.""" + from nest.nest import AuthorizationError, APIError if pin is not None: _LOGGER.debug("pin acquired, requesting access token") - nest.request_token(pin) + error_message = None + try: + nest.request_token(pin) + except AuthorizationError as auth_error: + error_message = "Nest authorization failed: {}".format(auth_error) + except APIError as api_error: + error_message = "Failed to call Nest API: {}".format(api_error) + + if error_message is not None: + _LOGGER.warning(error_message) + hass.components.configurator.async_notify_errors( + _CONFIGURING['nest'], error_message) + return False if nest.access_token is None: _LOGGER.debug("no access_token, requesting configuration") - request_configuration(nest, hass, config) - return + await async_request_configuration(nest, hass, config) + return False if 'nest' in _CONFIGURING: _LOGGER.debug("configuration done") configurator = hass.components.configurator - configurator.request_done(_CONFIGURING.pop('nest')) + configurator.async_request_done(_CONFIGURING.pop('nest')) _LOGGER.debug("proceeding with setup") conf = config[DOMAIN] hass.data[DATA_NEST] = NestDevice(hass, conf, nest) - _LOGGER.debug("proceeding with discovery") - discovery.load_platform(hass, 'climate', DOMAIN, {}, config) - discovery.load_platform(hass, 'camera', DOMAIN, {}, config) - - sensor_config = conf.get(CONF_SENSORS, {}) - discovery.load_platform(hass, 'sensor', DOMAIN, sensor_config, config) - - binary_sensor_config = conf.get(CONF_BINARY_SENSORS, {}) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, - binary_sensor_config, config) - - _LOGGER.debug("setup done") - - return True - - -def setup(hass, config): - """Set up the Nest thermostat component.""" - import nest - - if 'nest' in _CONFIGURING: - return - - conf = config[DOMAIN] - client_id = conf[CONF_CLIENT_ID] - client_secret = conf[CONF_CLIENT_SECRET] - filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) - - access_token_cache_file = hass.config.path(filename) - - nest = nest.Nest( - access_token_cache_file=access_token_cache_file, - client_id=client_id, client_secret=client_secret) - setup_nest(hass, nest, config) + for component, discovered in [ + ('climate', {}), + ('camera', {}), + ('sensor', conf.get(CONF_SENSORS, {})), + ('binary_sensor', conf.get(CONF_BINARY_SENSORS, {}))]: + _LOGGER.debug("proceeding with discovery -- %s", component) + hass.async_add_job(discovery.async_load_platform, + hass, component, DOMAIN, discovered, config) def set_mode(service): """Set the home/away mode for a Nest structure.""" @@ -148,9 +160,47 @@ def setup(hass, config): _LOGGER.error("Invalid structure %s", service.data[ATTR_STRUCTURE]) - hass.services.register( + hass.services.async_register( DOMAIN, 'set_mode', set_mode, schema=AWAY_SCHEMA) + def start_up(event): + """Start Nest update event listener.""" + hass.async_add_job(async_nest_update_event_broker, hass, nest) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up) + + def shut_down(event): + """Stop Nest update event listener.""" + if nest: + nest.update_event.set() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + + _LOGGER.debug("async_setup_nest is done") + + return True + + +async def async_setup(hass, config): + """Set up Nest components.""" + from nest import Nest + + if 'nest' in _CONFIGURING: + return + + conf = config[DOMAIN] + client_id = conf[CONF_CLIENT_ID] + client_secret = conf[CONF_CLIENT_SECRET] + filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) + + access_token_cache_file = hass.config.path(filename) + + nest = Nest( + access_token_cache_file=access_token_cache_file, + client_id=client_id, client_secret=client_secret) + + await async_setup_nest(hass, nest, config) + return True diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 0de2e2e0cdb..46a2206a9f7 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -7,13 +7,15 @@ https://home-assistant.io/components/sensor.nest/ from itertools import chain import logging -from homeassistant.components.nest import DATA_NEST +from homeassistant.components.nest import DATA_NEST, SIGNAL_NEST_UPDATE +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS, DEVICE_CLASS_TEMPERATURE) DEPENDENCIES = ['nest'] + SENSOR_TYPES = ['humidity', 'operation_mode', 'hvac_state'] @@ -130,6 +132,20 @@ class NestSensor(Entity): """Return the unit the value is expressed in.""" return self._unit + @property + def should_poll(self): + """Do not need poll thanks using Nest streaming API.""" + return False + + async def async_added_to_hass(self): + """Register update signal handler.""" + async def async_update_state(): + """Update sensor state.""" + await self.async_update_ha_state(True) + + async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, + async_update_state) + class NestBasicSensor(NestSensor): """Representation a basic Nest sensor.""" diff --git a/requirements_all.txt b/requirements_all.txt index 1c2f927eaf5..711defddb6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1033,7 +1033,7 @@ python-mpd2==1.0.0 python-mystrom==0.4.2 # homeassistant.components.nest -python-nest==4.0.0 +python-nest==4.0.1 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 From 0a724a54734f127eec0925538ab30ae70aa644ed Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jun 2018 10:52:25 -0400 Subject: [PATCH 106/137] Update frontend --- 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 5ebf6e8762f..fca9f33578a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180531.0'] +REQUIREMENTS = ['home-assistant-frontend==20180601.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 711defddb6e..4ed33a6dc5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180531.0 +home-assistant-frontend==20180601.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00b3d1f82e7..bbf01e1bfb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180531.0 +home-assistant-frontend==20180601.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From f5d74e07d51b0992d37105e1eb0fbbeca99a32ef Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Fri, 1 Jun 2018 12:04:54 -0400 Subject: [PATCH 107/137] Add support for outlets in HomeKit (#14628) --- homeassistant/components/homekit/__init__.py | 13 ++++-- homeassistant/components/homekit/const.py | 6 +++ .../components/homekit/type_switches.py | 39 ++++++++++++++++- homeassistant/components/homekit/util.py | 13 +++++- .../homekit/test_get_accessories.py | 23 +++++----- .../components/homekit/test_type_switches.py | 42 ++++++++++++++++++- tests/components/homekit/test_util.py | 9 ++-- 7 files changed, 123 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ce3b79e6c72..34372b8b6a8 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -12,7 +12,7 @@ import voluptuous as vol import homeassistant.components.cover as cover from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, DEVICE_CLASS_HUMIDITY, + CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, CONF_TYPE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -23,7 +23,7 @@ from homeassistant.util.decorator import Registry from .const import ( CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, CONF_FILTER, DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, - DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START) + DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, TYPE_OUTLET, TYPE_SWITCH) from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) @@ -38,6 +38,8 @@ STATUS_RUNNING = 1 STATUS_STOPPED = 2 STATUS_WAIT = 3 +SWITCH_TYPES = {TYPE_OUTLET: 'Outlet', + TYPE_SWITCH: 'Switch'} CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ @@ -149,8 +151,11 @@ def get_accessory(hass, driver, state, aid, config): elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'): a_type = 'LightSensor' - elif state.domain in ('automation', 'input_boolean', 'remote', 'script', - 'switch'): + elif state.domain == 'switch': + switch_type = config.get(CONF_TYPE, TYPE_SWITCH) + a_type = SWITCH_TYPES[switch_type] + + elif state.domain in ('automation', 'input_boolean', 'remote', 'script'): a_type = 'Switch' if a_type is None: diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 6d49c806e0f..dec6353850e 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -31,6 +31,10 @@ BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' MANUFACTURER = 'Home Assistant' +# #### Switch Types #### +TYPE_OUTLET = 'outlet' +TYPE_SWITCH = 'switch' + # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' @@ -46,6 +50,7 @@ SERV_LIGHTBULB = 'Lightbulb' SERV_LOCK = 'LockMechanism' SERV_MOTION_SENSOR = 'MotionSensor' SERV_OCCUPANCY_SENSOR = 'OccupancySensor' +SERV_OUTLET = 'Outlet' SERV_SECURITY_SYSTEM = 'SecuritySystem' SERV_SMOKE_SENSOR = 'SmokeSensor' SERV_SWITCH = 'Switch' @@ -84,6 +89,7 @@ CHAR_MODEL = 'Model' CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_NAME = 'Name' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' +CHAR_OUTLET_IN_USE = 'OutletInUse' CHAR_ON = 'On' CHAR_POSITION_STATE = 'PositionState' CHAR_ROTATION_DIRECTION = 'RotationDirection' diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 69f14821bd6..c8bf8c7ad7c 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,19 +1,54 @@ """Class to hold all switch accessories.""" import logging -from pyhap.const import CATEGORY_SWITCH +from pyhap.const import CATEGORY_OUTLET, CATEGORY_SWITCH +from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id from . import TYPES from .accessories import HomeAccessory -from .const import SERV_SWITCH, CHAR_ON +from .const import CHAR_ON, CHAR_OUTLET_IN_USE, SERV_OUTLET, SERV_SWITCH _LOGGER = logging.getLogger(__name__) +@TYPES.register('Outlet') +class Outlet(HomeAccessory): + """Generate an Outlet accessory.""" + + def __init__(self, *args): + """Initialize an Outlet accessory object.""" + super().__init__(*args, category=CATEGORY_OUTLET) + self.flag_target_state = False + + serv_outlet = self.add_preload_service(SERV_OUTLET) + self.char_on = serv_outlet.configure_char( + CHAR_ON, value=False, setter_callback=self.set_state) + self.char_outlet_in_use = serv_outlet.configure_char( + CHAR_OUTLET_IN_USE, value=True) + + 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.flag_target_state = True + params = {ATTR_ENTITY_ID: self.entity_id} + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + self.hass.services.call(SWITCH, service, params) + + def update_state(self, new_state): + """Update switch state after state changed.""" + 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.char_on.set_value(current_state) + self.flag_target_state = False + + @TYPES.register('Switch') class Switch(HomeAccessory): """Generate a Switch accessory.""" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 50095844757..6a43a0c6228 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -6,12 +6,13 @@ import voluptuous as vol import homeassistant.components.media_player as media_player from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, TEMP_CELSIUS) + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util from .const import ( CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, - FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE) + FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, TYPE_OUTLET, + TYPE_SWITCH) _LOGGER = logging.getLogger(__name__) @@ -35,6 +36,11 @@ MEDIA_PLAYER_SCHEMA = vol.Schema({ FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE))), }) +SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend({ + vol.Optional(CONF_TYPE, default=TYPE_SWITCH): vol.All( + cv.string, vol.In((TYPE_OUTLET, TYPE_SWITCH))), +}) + def validate_entity_config(values): """Validate config entry for CONF_ENTITY.""" @@ -62,6 +68,9 @@ def validate_entity_config(values): feature_list[key] = params config[CONF_FEATURE_LIST] = feature_list + elif domain == 'switch': + config = SWITCH_TYPE_SCHEMA(config) + else: config = BASIC_INFO_SCHEMA(config) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 3b7f307fce7..4de68057084 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -9,10 +9,11 @@ import homeassistant.components.climate as climate import homeassistant.components.media_player as media_player from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.components.homekit.const import ( - CONF_FEATURE_LIST, FEATURE_ON_OFF) + CONF_FEATURE_LIST, FEATURE_ON_OFF, TYPE_OUTLET, TYPE_SWITCH) from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_TYPE, TEMP_CELSIUS, + TEMP_FAHRENHEIT) def test_not_supported(caplog): @@ -129,17 +130,19 @@ def test_type_sensors(type_name, entity_id, state, attrs): assert mock_type.called -@pytest.mark.parametrize('type_name, entity_id, state, attrs', [ - ('Switch', 'automation.test', 'on', {}), - ('Switch', 'input_boolean.test', 'on', {}), - ('Switch', 'remote.test', 'on', {}), - ('Switch', 'script.test', 'on', {}), - ('Switch', 'switch.test', 'on', {}), +@pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ + ('Outlet', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_OUTLET}), + ('Switch', 'automation.test', 'on', {}, {}), + ('Switch', 'input_boolean.test', 'on', {}, {}), + ('Switch', 'remote.test', 'on', {}, {}), + ('Switch', 'script.test', 'on', {}, {}), + ('Switch', 'switch.test', 'on', {}, {}), + ('Switch', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_SWITCH}), ]) -def test_type_switches(type_name, entity_id, state, attrs): +def test_type_switches(type_name, entity_id, state, attrs, config): """Test if switch types are associated correctly.""" mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, None, entity_state, 2, {}) + get_accessory(None, None, entity_state, 2, config) assert mock_type.called diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index b1830d1926f..3a09d2715d1 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -2,12 +2,51 @@ import pytest from homeassistant.core import split_entity_id -from homeassistant.components.homekit.type_switches import Switch +from homeassistant.components.homekit.type_switches import Outlet, Switch from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from tests.common import async_mock_service +async def test_outlet_set_state(hass, hk_driver): + """Test if Outlet accessory and HA are updated accordingly.""" + entity_id = 'switch.outlet_test' + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Outlet(hass, hk_driver, 'Outlet', entity_id, 2, None) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 7 # Outlet + + assert acc.char_on.value is False + assert acc.char_outlet_in_use.value is True + + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_on.value is True + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_on.value is False + + # Set from HomeKit + call_turn_on = async_mock_service(hass, 'switch', 'turn_on') + call_turn_off = async_mock_service(hass, 'switch', 'turn_off') + + await hass.async_add_job(acc.char_on.client_update_value, True) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.char_on.client_update_value, False) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + + @pytest.mark.parametrize('entity_id', [ 'automation.test', 'input_boolean.test', @@ -23,6 +62,7 @@ async def test_switch_set_state(hass, hk_driver, entity_id): await hass.async_block_till_done() acc = Switch(hass, hk_driver, 'Switch', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() assert acc.aid == 2 assert acc.category == 8 # Switch diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 0bc1eb96841..fa9fddee5fc 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.core import State from homeassistant.components.homekit.const import ( CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, - FEATURE_PLAY_PAUSE) + FEATURE_PLAY_PAUSE, TYPE_OUTLET) from homeassistant.components.homekit.util import ( convert_to_float, density_to_air_quality, dismiss_setup_message, show_setup_message, temperature_to_homekit, temperature_to_states, @@ -15,7 +15,7 @@ from homeassistant.components.homekit.util import validate_entity_config \ from homeassistant.components.persistent_notification import ( ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN) from homeassistant.const import ( - ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, STATE_UNKNOWN, + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service @@ -30,7 +30,8 @@ def test_validate_entity_config(): {CONF_FEATURE: 'invalid_feature'}]}}, {'media_player.test': {CONF_FEATURE_LIST: [ {CONF_FEATURE: FEATURE_ON_OFF}, - {CONF_FEATURE: FEATURE_ON_OFF}]}}, ] + {CONF_FEATURE: FEATURE_ON_OFF}]}}, + {'switch.test': {CONF_TYPE: 'invalid_type'}}] for conf in configs: with pytest.raises(vol.Invalid): @@ -56,6 +57,8 @@ def test_validate_entity_config(): assert vec({'media_player.demo': config}) == \ {'media_player.demo': {CONF_FEATURE_LIST: {FEATURE_ON_OFF: {}, FEATURE_PLAY_PAUSE: {}}}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_OUTLET}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_OUTLET}} def test_validate_media_player_features(): From 4935043f4a17fd855f2f3e29e0b963700b191273 Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Fri, 1 Jun 2018 13:41:04 -0400 Subject: [PATCH 108/137] Add battery attribute to Sensibo (#14735) * Added battery attribute * Simplify current_battery --- homeassistant/components/climate/sensibo.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index 94d9612755c..b3fff0dd796 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -154,7 +154,8 @@ class SensiboClimate(ClimateDevice): @property def device_state_attributes(self): """Return the state attributes.""" - return {ATTR_CURRENT_HUMIDITY: self.current_humidity} + return {ATTR_CURRENT_HUMIDITY: self.current_humidity, + 'battery': self.current_battery} @property def temperature_unit(self): @@ -191,6 +192,11 @@ class SensiboClimate(ClimateDevice): """Return the current humidity.""" return self._measurements['humidity'] + @property + def current_battery(self): + """Return the current battery voltage.""" + return self._measurements.get('batteryVoltage') + @property def current_temperature(self): """Return the current temperature.""" From 3b8ee196bef4e1e272dad5a7548d4f83d7e8bb59 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jun 2018 19:41:20 +0200 Subject: [PATCH 109/137] Update syntax (#14742) --- homeassistant/components/counter/__init__.py | 36 ++++++++------------ 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 2df17a4e50a..03e5b273468 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -9,9 +9,9 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME) +from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state @@ -94,9 +94,8 @@ def async_reset(hass, entity_id): DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id})) -@asyncio.coroutine -def async_setup(hass, config): - """Set up a counter.""" +async def async_setup(hass, config): + """Set up the counters.""" component = EntityComponent(_LOGGER, DOMAIN, hass) entities = [] @@ -115,8 +114,7 @@ def async_setup(hass, config): if not entities: return False - @asyncio.coroutine - def async_handler_service(service): + async def async_handler_service(service): """Handle a call to the counter services.""" target_counters = component.async_extract_from_service(service) @@ -129,7 +127,7 @@ def async_setup(hass, config): tasks = [getattr(counter, attr)() for counter in target_counters] if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_INCREMENT, async_handler_service) @@ -138,7 +136,7 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_RESET, async_handler_service) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -181,30 +179,26 @@ class Counter(Entity): ATTR_STEP: self._step, } - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" # If not None, we got an initial value. if self._state is not None: return - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) self._state = state and state.state == state - @asyncio.coroutine - def async_decrement(self): + async def async_decrement(self): """Decrement the counter.""" self._state -= self._step - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_increment(self): + async def async_increment(self): """Increment a counter.""" self._state += self._step - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_reset(self): + async def async_reset(self): """Reset a counter.""" self._state = self._initial - yield from self.async_update_ha_state() + await self.async_update_ha_state() From 77dca8272c4bd41a91c1e6266b85104cd3f72a18 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jun 2018 19:41:35 +0200 Subject: [PATCH 110/137] Upgrade blockchain to 1.4.4 (#14738) --- homeassistant/components/sensor/bitcoin.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index 8bed72a67c2..38d2226012c 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -15,7 +15,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['blockchain==1.4.0'] +REQUIREMENTS = ['blockchain==1.4.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 4ed33a6dc5c..c2aa16483da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,7 @@ blinkstick==1.1.8 # blinkt==0.1.0 # homeassistant.components.sensor.bitcoin -blockchain==1.4.0 +blockchain==1.4.4 # homeassistant.components.light.decora # bluepy==1.1.4 From d6e76969ccd0a7943dec5d0096170d732ed16953 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jun 2018 23:33:04 +0200 Subject: [PATCH 111/137] Tweak about the requirements --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9a8e6812cf3..c2f65f9a8be 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -20,7 +20,7 @@ If user exposed functionality or configuration variables are added/changed: If the code communicates with devices, web services, or third-party tools: - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - - [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. + - [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. - [ ] New files were added to `.coveragerc`. If the code does not interact with devices: From cfac537f5115307519466ccc57b79b7ecb99ca99 Mon Sep 17 00:00:00 2001 From: austinlg96 Date: Sat, 2 Jun 2018 03:23:51 -0400 Subject: [PATCH 112/137] Added option to block Osram Lightify individual lights in the same way that groups can be (#14470) --- .../components/light/osramlightify.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index 2c44620caca..939d0fe6988 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -27,8 +27,10 @@ REQUIREMENTS = ['lightify==1.0.6.1'] _LOGGER = logging.getLogger(__name__) +CONF_ALLOW_LIGHTIFY_NODES = 'allow_lightify_nodes' CONF_ALLOW_LIGHTIFY_GROUPS = 'allow_lightify_groups' +DEFAULT_ALLOW_LIGHTIFY_NODES = True DEFAULT_ALLOW_LIGHTIFY_GROUPS = True MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -40,6 +42,8 @@ SUPPORT_OSRAMLIGHTIFY = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_ALLOW_LIGHTIFY_NODES, + default=DEFAULT_ALLOW_LIGHTIFY_NODES): cv.boolean, vol.Optional(CONF_ALLOW_LIGHTIFY_GROUPS, default=DEFAULT_ALLOW_LIGHTIFY_GROUPS): cv.boolean, }) @@ -50,6 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): import lightify host = config.get(CONF_HOST) + add_nodes = config.get(CONF_ALLOW_LIGHTIFY_NODES) add_groups = config.get(CONF_ALLOW_LIGHTIFY_GROUPS) try: @@ -60,10 +65,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.exception(msg) return - setup_bridge(bridge, add_devices, add_groups) + setup_bridge(bridge, add_devices, add_nodes, add_groups) -def setup_bridge(bridge, add_devices, add_groups): +def setup_bridge(bridge, add_devices, add_nodes, add_groups): """Set up the Lightify bridge.""" lights = {} @@ -80,14 +85,15 @@ def setup_bridge(bridge, add_devices, add_groups): new_lights = [] - for (light_id, light) in bridge.lights().items(): - if light_id not in lights: - osram_light = OsramLightifyLight( - light_id, light, update_lights) - lights[light_id] = osram_light - new_lights.append(osram_light) - else: - lights[light_id].light = light + if add_nodes: + for (light_id, light) in bridge.lights().items(): + if light_id not in lights: + osram_light = OsramLightifyLight( + light_id, light, update_lights) + lights[light_id] = osram_light + new_lights.append(osram_light) + else: + lights[light_id].light = light if add_groups: for (group_name, group) in bridge.groups().items(): From e7985c970b6b1b01abf665322ab6e18d731e624a Mon Sep 17 00:00:00 2001 From: Tristan Caulfield Date: Sat, 2 Jun 2018 02:30:15 -0500 Subject: [PATCH 113/137] Upgrade directpy to 0.5 (#14750) * Version Requirement bump Bump required version to 0.5 to allow component to work with Genie Mini clients using the clientAddr variable. * Ran script/gen_requirements_all.py as requested. --- homeassistant/components/media_player/directv.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index 25d13e3017a..0adb02b6a65 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_DEVICE, CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING, CONF_PORT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['directpy==0.2'] +REQUIREMENTS = ['directpy==0.5'] DEFAULT_DEVICE = '0' DEFAULT_NAME = 'DirecTV Receiver' diff --git a/requirements_all.txt b/requirements_all.txt index c2aa16483da..8e9c37b75e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -249,7 +249,7 @@ deluge-client==1.4.0 denonavr==0.7.2 # homeassistant.components.media_player.directv -directpy==0.2 +directpy==0.5 # homeassistant.components.sensor.discogs discogs_client==2.2.1 From ad86e68c1eafd6109c887801d977658f737bcb5c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 12:00:01 +0200 Subject: [PATCH 114/137] Update syntax of platform random (#14767) --- homeassistant/components/binary_sensor/random.py | 8 +++----- homeassistant/components/sensor/random.py | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/binary_sensor/random.py b/homeassistant/components/binary_sensor/random.py index 162d0480389..ab6c1e5d479 100644 --- a/homeassistant/components/binary_sensor/random.py +++ b/homeassistant/components/binary_sensor/random.py @@ -4,7 +4,6 @@ Support for showing random states. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.random/ """ -import asyncio import logging import voluptuous as vol @@ -24,8 +23,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@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 Random binary sensor.""" name = config.get(CONF_NAME) device_class = config.get(CONF_DEVICE_CLASS) @@ -57,8 +56,7 @@ class RandomSensor(BinarySensorDevice): """Return the sensor class of the sensor.""" return self._device_class - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get new state and update the sensor's state.""" from random import getrandbits self._state = bool(getrandbits(1)) diff --git a/homeassistant/components/sensor/random.py b/homeassistant/components/sensor/random.py index e57bbcc3955..c3ff08a5781 100644 --- a/homeassistant/components/sensor/random.py +++ b/homeassistant/components/sensor/random.py @@ -4,7 +4,6 @@ Support for showing random numbers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.random/ """ -import asyncio import logging import voluptuous as vol @@ -34,8 +33,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@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 Random number sensor.""" name = config.get(CONF_NAME) minimum = config.get(CONF_MINIMUM) @@ -84,8 +83,7 @@ class RandomSensor(Entity): ATTR_MINIMUM: self._minimum, } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get a new number and updates the states.""" from random import randrange self._state = randrange(self._minimum, self._maximum + 1) From fe0e49db4bfca77fa36f408bbcf32b55598863b0 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 2 Jun 2018 13:45:48 +0200 Subject: [PATCH 115/137] Update postnl api to 1.0.2 (#14769) --- homeassistant/components/sensor/postnl.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/postnl.py b/homeassistant/components/sensor/postnl.py index c38f58b7916..63a9c1d67d5 100644 --- a/homeassistant/components/sensor/postnl.py +++ b/homeassistant/components/sensor/postnl.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['postnl_api==1.0.1'] +REQUIREMENTS = ['postnl_api==1.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8e9c37b75e1..4a6560133ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -654,7 +654,7 @@ pmsensor==0.4 pocketcasts==0.1 # homeassistant.components.sensor.postnl -postnl_api==1.0.1 +postnl_api==1.0.2 # homeassistant.components.climate.proliphix proliphix==0.4.1 From 875e05ff387e0fe1a99e3fad14075b13688021fb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:29:38 +0200 Subject: [PATCH 116/137] Remove swagger file (#14762) --- docs/swagger.yaml | 606 ---------------------------------------------- 1 file changed, 606 deletions(-) delete mode 100644 docs/swagger.yaml diff --git a/docs/swagger.yaml b/docs/swagger.yaml deleted file mode 100644 index 488d6bddd46..00000000000 --- a/docs/swagger.yaml +++ /dev/null @@ -1,606 +0,0 @@ -swagger: '2.0' -info: - title: Home Assistant - description: Home Assistant REST API - version: "1.0.1" -# the domain of the service -host: localhost:8123 - -# array of all schemes that your API supports -schemes: - - http - - https - -securityDefinitions: - #api_key: - # type: apiKey - # description: API password - # name: api_password - # in: query - - api_key: - type: apiKey - description: API password - name: x-ha-access - in: header - -# will be prefixed to all paths -basePath: /api - -consumes: - - application/json -produces: - - application/json -paths: - /: - get: - summary: API alive message - description: Returns message if API is up and running. - tags: - - Core - security: - - api_key: [] - responses: - 200: - description: API is up and running - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /config: - get: - summary: API alive message - description: Returns the current configuration as JSON. - tags: - - Core - security: - - api_key: [] - responses: - 200: - description: Current configuration - schema: - $ref: '#/definitions/ApiConfig' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /discovery_info: - get: - summary: Basic information about Home Assistant instance - tags: - - Core - responses: - 200: - description: Basic information - schema: - $ref: '#/definitions/DiscoveryInfo' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /bootstrap: - get: - summary: Returns all data needed to bootstrap Home Assistant. - tags: - - Core - security: - - api_key: [] - responses: - 200: - description: Bootstrap information - schema: - $ref: '#/definitions/BootstrapInfo' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /events: - get: - summary: Array of event objects. - description: Returns an array of event objects. Each event object contain event name and listener count. - tags: - - Events - security: - - api_key: [] - responses: - 200: - description: Events - schema: - type: array - items: - $ref: '#/definitions/Event' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /services: - get: - summary: Array of service objects. - description: Returns an array of service objects. Each object contains the domain and which services it contains. - tags: - - Services - security: - - api_key: [] - responses: - 200: - description: Services - schema: - type: array - items: - $ref: '#/definitions/Service' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /history: - get: - summary: Array of state changes in the past. - description: Returns an array of state changes in the past. Each object contains further detail for the entities. - tags: - - State - security: - - api_key: [] - responses: - 200: - description: State changes - schema: - type: array - items: - $ref: '#/definitions/History' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /states: - get: - summary: Array of state objects. - description: | - Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes. - tags: - - State - security: - - api_key: [] - responses: - 200: - description: States - schema: - type: array - items: - $ref: '#/definitions/State' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /states/{entity_id}: - get: - summary: Specific state object. - description: | - Returns a state object for specified entity_id. - tags: - - State - security: - - api_key: [] - parameters: - - name: entity_id - in: path - description: entity_id of the entity to query - required: true - type: string - responses: - 200: - description: State - schema: - $ref: '#/definitions/State' - 404: - description: Not found - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - post: - description: | - Updates or creates the current state of an entity. - tags: - - State - consumes: - - application/json - parameters: - - name: entity_id - in: path - description: entity_id to set the state of - required: true - type: string - - $ref: '#/parameters/State' - responses: - 200: - description: State of existing entity was set - schema: - $ref: '#/definitions/State' - 201: - description: State of new entity was set - schema: - $ref: '#/definitions/State' - headers: - location: - type: string - description: location of the new entity - default: - description: Error - schema: - $ref: '#/definitions/Message' - /error_log: - get: - summary: Error log - description: | - Retrieve all errors logged during the current session of Home Assistant as a plaintext response. - tags: - - Core - security: - - api_key: [] - produces: - - text/plain - responses: - 200: - description: Plain text error log - default: - description: Error - schema: - $ref: '#/definitions/Message' - /camera_proxy/camera.{entity_id}: - get: - summary: Camera image. - description: | - Returns the data (image) from the specified camera entity_id. - tags: - - Camera - security: - - api_key: [] - produces: - - image/jpeg - parameters: - - name: entity_id - in: path - description: entity_id of the camera to query - required: true - type: string - responses: - 200: - description: Camera image - schema: - type: file - default: - description: Error - schema: - $ref: '#/definitions/Message' - /events/{event_type}: - post: - description: | - Fires an event with event_type - tags: - - Events - security: - - api_key: [] - consumes: - - application/json - parameters: - - name: event_type - in: path - description: event_type to fire event with - required: true - type: string - - $ref: '#/parameters/EventData' - responses: - 200: - description: Response message - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /services/{domain}/{service}: - post: - description: | - Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first. - tags: - - Services - security: - - api_key: [] - consumes: - - application/json - parameters: - - name: domain - in: path - description: domain of the service - required: true - type: string - - name: service - in: path - description: service to call - required: true - type: string - - $ref: '#/parameters/ServiceData' - responses: - 200: - description: List of states that have changed while the service was being executed. The result will include any changed states that changed while the service was being executed, even if their change was the result of something else happening in the system. - schema: - type: array - items: - $ref: '#/definitions/State' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /template: - post: - description: | - Render a Home Assistant template. - tags: - - Template - security: - - api_key: [] - consumes: - - application/json - produces: - - text/plain - parameters: - - $ref: '#/parameters/Template' - responses: - 200: - description: Returns the rendered template in plain text. - schema: - type: string - default: - description: Error - schema: - $ref: '#/definitions/Message' - /event_forwarding: - post: - description: | - Setup event forwarding to another Home Assistant instance. - tags: - - Core - security: - - api_key: [] - consumes: - - application/json - parameters: - - $ref: '#/parameters/EventForwarding' - responses: - 200: - description: It will return a message if event forwarding was setup successful. - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - delete: - description: | - Cancel event forwarding to another Home Assistant instance. - tags: - - Core - consumes: - - application/json - parameters: - - $ref: '#/parameters/EventForwarding' - responses: - 200: - description: It will return a message if event forwarding was cancelled successful. - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /stream: - get: - summary: Server-sent events - description: The server-sent events feature is a one-way channel from your Home Assistant server to a client which is acting as a consumer. - tags: - - Core - - Events - security: - - api_key: [] - produces: - - text/event-stream - parameters: - - name: restrict - in: query - description: comma-separated list of event_types to filter - required: false - type: string - responses: - default: - description: Stream of events - schema: - type: object - x-events: - state_changed: - type: object - properties: - entity_id: - type: string - old_state: - $ref: '#/definitions/State' - new_state: - $ref: '#/definitions/State' -definitions: - ApiConfig: - type: object - properties: - components: - type: array - description: List of component types - items: - type: string - description: Component type - latitude: - type: number - format: float - description: Latitude of Home Assistant server - longitude: - type: number - format: float - description: Longitude of Home Assistant server - location_name: - type: string - unit_system: - type: object - properties: - length: - type: string - mass: - type: string - temperature: - type: string - volume: - type: string - time_zone: - type: string - version: - type: string - DiscoveryInfo: - type: object - properties: - base_url: - type: string - location_name: - type: string - requires_api_password: - type: boolean - version: - type: string - BootstrapInfo: - type: object - properties: - config: - $ref: '#/definitions/ApiConfig' - events: - type: array - items: - $ref: '#/definitions/Event' - services: - type: array - items: - $ref: '#/definitions/Service' - states: - type: array - items: - $ref: '#/definitions/State' - Event: - type: object - properties: - event: - type: string - listener_count: - type: integer - Service: - type: object - properties: - domain: - type: string - services: - type: object - additionalProperties: - $ref: '#/definitions/DomainService' - DomainService: - type: object - properties: - description: - type: string - fields: - type: object - description: Object with service fields that can be called - State: - type: object - properties: - attributes: - $ref: '#/definitions/StateAttributes' - state: - type: string - entity_id: - type: string - last_changed: - type: string - format: date-time - StateAttributes: - type: object - additionalProperties: - type: string - History: - allOf: - - $ref: '#/definitions/State' - - type: object - properties: - last_updated: - type: string - format: date-time - Message: - type: object - properties: - message: - type: string -parameters: - State: - name: body - in: body - description: State parameter - required: false - schema: - type: object - required: - - state - properties: - attributes: - $ref: '#/definitions/StateAttributes' - state: - type: string - EventData: - name: body - in: body - description: event_data - required: false - schema: - type: object - ServiceData: - name: body - in: body - description: service_data - required: false - schema: - type: object - Template: - name: body - in: body - description: Template to render - required: true - schema: - type: object - required: - - template - properties: - template: - description: Jinja2 template string - type: string - EventForwarding: - name: body - in: body - description: Event Forwarding parameter - required: true - schema: - type: object - required: - - host - - api_password - properties: - host: - type: string - api_password: - type: string - port: - type: integer From 1ce4c2092a6dd8662d91a3cf4e5df66c5c68098c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:30:07 +0200 Subject: [PATCH 117/137] Update syntax (#14771) --- homeassistant/components/sensor/version.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/version.py b/homeassistant/components/sensor/version.py index c19d2743563..db61d059783 100644 --- a/homeassistant/components/sensor/version.py +++ b/homeassistant/components/sensor/version.py @@ -4,7 +4,6 @@ Support for displaying the current version of Home Assistant. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.version/ """ -import asyncio import logging import voluptuous as vol @@ -23,8 +22,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@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 Version sensor platform.""" name = config.get(CONF_NAME) From 74b7dabf2d38b636962ff10a28e4207bf9d446eb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:30:54 +0200 Subject: [PATCH 118/137] Update syntax (#14768) --- homeassistant/components/sensor/worldclock.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/worldclock.py b/homeassistant/components/sensor/worldclock.py index 839b5776b3c..1240480d4a3 100644 --- a/homeassistant/components/sensor/worldclock.py +++ b/homeassistant/components/sensor/worldclock.py @@ -4,7 +4,6 @@ Support for showing the time in a different time zone. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.worldclock/ """ -import asyncio import logging import voluptuous as vol @@ -29,8 +28,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@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 World clock sensor.""" name = config.get(CONF_NAME) time_zone = dt_util.get_time_zone(config.get(CONF_TIME_ZONE)) @@ -62,8 +61,7 @@ class WorldClockSensor(Entity): """Icon to use in the frontend, if any.""" return ICON - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the time and updates the states.""" self._state = dt_util.now(time_zone=self._time_zone).strftime( TIME_STR_FORMAT) From b86cd325fe31997a202dd67871f2f37254412997 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:31:06 +0200 Subject: [PATCH 119/137] Update syntax (#14770) --- homeassistant/components/sensor/uptime.py | 31 ++++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sensor/uptime.py b/homeassistant/components/sensor/uptime.py index 91746af71f1..7e893899815 100644 --- a/homeassistant/components/sensor/uptime.py +++ b/homeassistant/components/sensor/uptime.py @@ -1,25 +1,25 @@ """ -Component to retrieve uptime for Home Assistant. +Platform to retrieve uptime for Home Assistant. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.uptime/ """ -import asyncio import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_NAME, CONF_UNIT_OF_MEASUREMENT) +from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Uptime' +ICON = 'mdi:clock' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT, default='days'): @@ -27,22 +27,22 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@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 uptime sensor platform.""" name = config.get(CONF_NAME) units = config.get(CONF_UNIT_OF_MEASUREMENT) + async_add_devices([UptimeSensor(name, units)], True) class UptimeSensor(Entity): """Representation of an uptime sensor.""" - def __init__(self, name, units): + def __init__(self, name, unit): """Initialize the uptime sensor.""" self._name = name - self._icon = 'mdi:clock' - self._units = units + self._unit = unit self.initial = dt_util.now() self._state = None @@ -54,27 +54,28 @@ class UptimeSensor(Entity): @property def icon(self): """Icon to display in the front end.""" - return self._icon + return ICON @property def unit_of_measurement(self): """Return the unit of measurement the value is expressed in.""" - return self._units + return self._unit @property def state(self): """Return the state of the sensor.""" return self._state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the state of the sensor.""" delta = dt_util.now() - self.initial div_factor = 3600 + if self.unit_of_measurement == 'days': div_factor *= 24 elif self.unit_of_measurement == 'minutes': div_factor /= 60 + delta = delta.total_seconds() / div_factor self._state = round(delta, 2) _LOGGER.debug("New value: %s", delta) From 5aaf81f2c990e8b74948e348458b5f71f3189403 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:31:43 +0200 Subject: [PATCH 120/137] Upgrade Sphinx to 1.7.5 (#14764) --- requirements_docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index 5ef38e1537e..0556b35fc08 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.7.4 +Sphinx==1.7.5 sphinx-autodoc-typehints==1.3.0 sphinx-autodoc-annotation==1.0.post1 From f2dacb25701cb81418aae075b5d3708f72b0e1ec Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:33:48 +0200 Subject: [PATCH 121/137] Upgrade youtube_dl to 2018.06.02 (#14763) --- 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 73837ce2ca1..75b90b084fc 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.05.26'] +REQUIREMENTS = ['youtube_dl==2018.06.02'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 4a6560133ce..09526e3cd74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1397,7 +1397,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.05.26 +youtube_dl==2018.06.02 # homeassistant.components.light.zengge zengge==0.2 From a8413249c2c09f0d3960500417381b6a6cdb5b7a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:34:30 +0200 Subject: [PATCH 122/137] Upgrade sqlalchemy to 1.2.8 (#14765) --- homeassistant/components/recorder/__init__.py | 2 +- homeassistant/components/sensor/sql.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 9b5bea043f4..38ba593261f 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.7'] +REQUIREMENTS = ['sqlalchemy==1.2.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index b7ece1bdb87..7fefb0f450b 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.7'] +REQUIREMENTS = ['sqlalchemy==1.2.8'] CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' diff --git a/requirements_all.txt b/requirements_all.txt index 09526e3cd74..fd11f6d48cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1251,7 +1251,7 @@ spotcrime==1.0.3 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.7 +sqlalchemy==1.2.8 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bbf01e1bfb4..3d870737aac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -188,7 +188,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.7 +sqlalchemy==1.2.8 # homeassistant.components.statsd statsd==3.2.1 From 27df4cca6c43df63ad8c78cc362065961a33c809 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:34:47 +0200 Subject: [PATCH 123/137] Upgrade shodan to 1.8.1 (#14760) --- homeassistant/components/sensor/shodan.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py index 1cc2ba30866..bc3e127508b 100644 --- a/homeassistant/components/sensor/shodan.py +++ b/homeassistant/components/sensor/shodan.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['shodan==1.8.0'] +REQUIREMENTS = ['shodan==1.8.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fd11f6d48cf..820a77531df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1202,7 +1202,7 @@ sense_energy==0.3.1 sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan -shodan==1.8.0 +shodan==1.8.1 # homeassistant.components.notify.simplepush simplepush==1.1.4 From 28ef94c3fa383830d3f3b9ce9f52565afa6f9356 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 15:08:10 +0200 Subject: [PATCH 124/137] Update syntax (#14772) --- homeassistant/components/sensor/simulated.py | 38 +++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/sensor/simulated.py b/homeassistant/components/sensor/simulated.py index ae2d4939eab..9dac0b48bc2 100644 --- a/homeassistant/components/sensor/simulated.py +++ b/homeassistant/components/sensor/simulated.py @@ -4,51 +4,48 @@ Adds a simulated sensor. For more details about this platform, refer to the documentation at https://home-assistant.io/components/sensor.simulated/ """ -import asyncio -import datetime as datetime +import logging import math from random import Random -import logging import voluptuous as vol -import homeassistant.util.dt as dt_util -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = datetime.timedelta(seconds=30) -ICON = 'mdi:chart-line' -CONF_UNIT = 'unit' CONF_AMP = 'amplitude' +CONF_FWHM = 'spread' CONF_MEAN = 'mean' CONF_PERIOD = 'period' CONF_PHASE = 'phase' -CONF_FWHM = 'spread' CONF_SEED = 'seed' +CONF_UNIT = 'unit' -DEFAULT_NAME = 'simulated' -DEFAULT_UNIT = 'value' DEFAULT_AMP = 1 +DEFAULT_FWHM = 0 DEFAULT_MEAN = 0 +DEFAULT_NAME = 'simulated' DEFAULT_PERIOD = 60 DEFAULT_PHASE = 0 -DEFAULT_FWHM = 0 DEFAULT_SEED = 999 +DEFAULT_UNIT = 'value' +ICON = 'mdi:chart-line' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): cv.string, vol.Optional(CONF_AMP, default=DEFAULT_AMP): vol.Coerce(float), + vol.Optional(CONF_FWHM, default=DEFAULT_FWHM): vol.Coerce(float), vol.Optional(CONF_MEAN, default=DEFAULT_MEAN): vol.Coerce(float), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PERIOD, default=DEFAULT_PERIOD): cv.positive_int, vol.Optional(CONF_PHASE, default=DEFAULT_PHASE): vol.Coerce(float), - vol.Optional(CONF_FWHM, default=DEFAULT_FWHM): vol.Coerce(float), vol.Optional(CONF_SEED, default=DEFAULT_SEED): cv.positive_int, + vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): cv.string, }) @@ -63,9 +60,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): fwhm = config.get(CONF_FWHM) seed = config.get(CONF_SEED) - sensor = SimulatedSensor( - name, unit, amp, mean, period, phase, fwhm, seed - ) + sensor = SimulatedSensor(name, unit, amp, mean, period, phase, fwhm, seed) add_devices([sensor], True) @@ -107,8 +102,7 @@ class SimulatedSensor(Entity): noise = self._random.gauss(mu=0, sigma=fwhm) return mean + periodic + noise - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the sensor.""" self._state = self.signal_calc() From 12e679c14daf9fac8c68a3e8c314cde1a980f21f Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sat, 2 Jun 2018 18:54:48 -0700 Subject: [PATCH 125/137] Assign device class to nest sensors (#14746) * Assign device class to nest sensors sensor/nest.NestSensor => /nest.NestSensorDevice, so that BinarySensor platform do not reference Sensor platform anymore * Resolve code review comment * Follow code review comment --- .../components/binary_sensor/nest.py | 54 ++++--- homeassistant/components/nest.py | 55 ++++++- homeassistant/components/sensor/nest.py | 151 ++++++------------ 3 files changed, 130 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 008b6eed1e4..882ff142e8c 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -7,32 +7,31 @@ https://home-assistant.io/components/binary_sensor.nest/ from itertools import chain import logging -from homeassistant.components.binary_sensor import (BinarySensorDevice) -from homeassistant.components.nest import DATA_NEST -from homeassistant.components.sensor.nest import NestSensor +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.nest import DATA_NEST, NestSensorDevice from homeassistant.const import CONF_MONITORED_CONDITIONS DEPENDENCIES = ['nest'] -BINARY_TYPES = ['online'] +BINARY_TYPES = {'online': 'connectivity'} -CLIMATE_BINARY_TYPES = [ - 'fan', - 'is_using_emergency_heat', - 'is_locked', - 'has_leaf', -] +CLIMATE_BINARY_TYPES = { + 'fan': None, + 'is_using_emergency_heat': 'heat', + 'is_locked': None, + 'has_leaf': None, +} -CAMERA_BINARY_TYPES = [ - 'motion_detected', - 'sound_detected', - 'person_detected', -] +CAMERA_BINARY_TYPES = { + 'motion_detected': 'motion', + 'sound_detected': 'sound', + 'person_detected': 'occupancy', +} -STRUCTURE_BINARY_TYPES = [ - 'away', - # 'security_state', # wait for pending python-nest update -] +STRUCTURE_BINARY_TYPES = { + 'away': None, + # 'security_state', # pending python-nest update +} STRUCTURE_BINARY_STATE_MAP = { 'away': {'away': True, 'home': False}, @@ -50,8 +49,8 @@ _BINARY_TYPES_DEPRECATED = [ 'hvac_emer_heat_state', ] -_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \ - + CAMERA_BINARY_TYPES + STRUCTURE_BINARY_TYPES +_VALID_BINARY_SENSOR_TYPES = {**BINARY_TYPES, **CLIMATE_BINARY_TYPES, + **CAMERA_BINARY_TYPES, **STRUCTURE_BINARY_TYPES} _LOGGER = logging.getLogger(__name__) @@ -105,7 +104,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(sensors, True) -class NestBinarySensor(NestSensor, BinarySensorDevice): +class NestBinarySensor(NestSensorDevice, BinarySensorDevice): """Represents a Nest binary sensor.""" @property @@ -113,6 +112,11 @@ class NestBinarySensor(NestSensor, BinarySensorDevice): """Return true if the binary sensor is on.""" return self._state + @property + def device_class(self): + """Return the device class of the binary sensor.""" + return _VALID_BINARY_SENSOR_TYPES.get(self.variable) + def update(self): """Retrieve latest state.""" value = getattr(self.device, self.variable) @@ -133,9 +137,9 @@ class NestActivityZoneSensor(NestBinarySensor): self._name = "{} {} activity".format(self._name, self.zone.name) @property - def name(self): - """Return the name of the nest, if any.""" - return self._name + def device_class(self): + """Return the device class of the binary sensor.""" + return 'motion' def update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 365f0593c8d..16a0b80d1fd 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -15,7 +15,9 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery, config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send, \ + async_dispatcher_connect +from homeassistant.helpers.entity import Entity REQUIREMENTS = ['python-nest==4.0.1'] @@ -272,3 +274,54 @@ class NestDevice(object): except socket.error: _LOGGER.error( "Connection error logging into the nest web service.") + + +class NestSensorDevice(Entity): + """Representation of a Nest sensor.""" + + def __init__(self, structure, device, variable): + """Initialize the sensor.""" + self.structure = structure + self.variable = variable + + if device is not None: + # device specific + self.device = device + self._name = "{} {}".format(self.device.name_long, + self.variable.replace('_', ' ')) + else: + # structure only + self.device = structure + self._name = "{} {}".format(self.structure.name, + self.variable.replace('_', ' ')) + + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the nest, if any.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @property + def should_poll(self): + """Do not need poll thanks using Nest streaming API.""" + return False + + def update(self): + """Do not use NestSensorDevice directly.""" + raise NotImplementedError + + async def async_added_to_hass(self): + """Register update signal handler.""" + async def async_update_state(): + """Update sensor state.""" + await self.async_update_ha_state(True) + + async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, + async_update_state) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 46a2206a9f7..00d18c7fe10 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -4,49 +4,44 @@ Support for Nest Thermostat Sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.nest/ """ -from itertools import chain import logging -from homeassistant.components.nest import DATA_NEST, SIGNAL_NEST_UPDATE -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.components.nest import DATA_NEST, NestSensorDevice from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS, - DEVICE_CLASS_TEMPERATURE) + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY) DEPENDENCIES = ['nest'] -SENSOR_TYPES = ['humidity', - 'operation_mode', - 'hvac_state'] +SENSOR_TYPES = ['humidity', 'operation_mode', 'hvac_state'] -SENSOR_TYPES_DEPRECATED = ['last_ip', - 'local_ip', - 'last_connection'] +TEMP_SENSOR_TYPES = ['temperature', 'target'] -DEPRECATED_WEATHER_VARS = {'weather_humidity': 'humidity', - 'weather_temperature': 'temperature', - 'weather_condition': 'condition', - 'wind_speed': 'kph', - 'wind_direction': 'direction'} - -SENSOR_UNITS = {'humidity': '%', 'temperature': '°C'} - -PROTECT_VARS = ['co_status', 'smoke_status', 'battery_health'] - -PROTECT_VARS_DEPRECATED = ['battery_level'] - -SENSOR_TEMP_TYPES = ['temperature', 'target'] +PROTECT_SENSOR_TYPES = ['co_status', 'smoke_status', 'battery_health'] STRUCTURE_SENSOR_TYPES = ['eta'] +_VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \ + + STRUCTURE_SENSOR_TYPES + +SENSOR_UNITS = {'humidity': '%'} + +SENSOR_DEVICE_CLASSES = {'humidity': DEVICE_CLASS_HUMIDITY} + VARIABLE_NAME_MAPPING = {'eta': 'eta_begin', 'operation_mode': 'mode'} -_SENSOR_TYPES_DEPRECATED = SENSOR_TYPES_DEPRECATED \ - + list(DEPRECATED_WEATHER_VARS.keys()) + PROTECT_VARS_DEPRECATED +SENSOR_TYPES_DEPRECATED = ['last_ip', + 'local_ip', + 'last_connection', + 'battery_level'] -_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS \ - + STRUCTURE_SENSOR_TYPES +DEPRECATED_WEATHER_VARS = ['weather_humidity', + 'weather_temperature', + 'weather_condition', + 'wind_speed', + 'wind_direction'] + +_SENSOR_TYPES_DEPRECATED = SENSOR_TYPES_DEPRECATED + DEPRECATED_WEATHER_VARS _LOGGER = logging.getLogger(__name__) @@ -76,7 +71,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): "monitored_conditions. See " "https://home-assistant.io/components/" "binary_sensor.nest/ for valid options.") - _LOGGER.error(wstr) all_sensors = [] @@ -84,70 +78,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): all_sensors += [NestBasicSensor(structure, None, variable) for variable in conditions if variable in STRUCTURE_SENSOR_TYPES] - for structure, device in chain(nest.thermostats(), nest.smoke_co_alarms()): - sensors = [NestBasicSensor(structure, device, variable) - for variable in conditions - if variable in SENSOR_TYPES and device.is_thermostat] - sensors += [NestTempSensor(structure, device, variable) - for variable in conditions - if variable in SENSOR_TEMP_TYPES and device.is_thermostat] - sensors += [NestProtectSensor(structure, device, variable) - for variable in conditions - if variable in PROTECT_VARS and device.is_smoke_co_alarm] - all_sensors.extend(sensors) + + for structure, device in nest.thermostats(): + all_sensors += [NestBasicSensor(structure, device, variable) + for variable in conditions + if variable in SENSOR_TYPES] + all_sensors += [NestTempSensor(structure, device, variable) + for variable in conditions + if variable in TEMP_SENSOR_TYPES] + + for structure, device in nest.smoke_co_alarms(): + all_sensors += [NestBasicSensor(structure, device, variable) + for variable in conditions + if variable in PROTECT_SENSOR_TYPES] add_devices(all_sensors, True) -class NestSensor(Entity): - """Representation of a Nest sensor.""" - - def __init__(self, structure, device, variable): - """Initialize the sensor.""" - self.structure = structure - self.variable = variable - - if device is not None: - # device specific - self.device = device - self._location = self.device.where - self._name = "{} {}".format(self.device.name_long, - self.variable.replace('_', ' ')) - else: - # structure only - self.device = structure - self._name = "{} {}".format(self.structure.name, - self.variable.replace('_', ' ')) - - self._state = None - self._unit = None - - @property - def name(self): - """Return the name of the nest, if any.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - - @property - def should_poll(self): - """Do not need poll thanks using Nest streaming API.""" - return False - - async def async_added_to_hass(self): - """Register update signal handler.""" - async def async_update_state(): - """Update sensor state.""" - await self.async_update_ha_state(True) - - async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, - async_update_state) - - -class NestBasicSensor(NestSensor): +class NestBasicSensor(NestSensorDevice): """Representation a basic Nest sensor.""" @property @@ -155,18 +103,26 @@ class NestBasicSensor(NestSensor): """Return the state of the sensor.""" return self._state + @property + def device_class(self): + """Return the device class of the sensor.""" + return SENSOR_DEVICE_CLASSES.get(self.variable) + def update(self): """Retrieve latest state.""" - self._unit = SENSOR_UNITS.get(self.variable, None) + self._unit = SENSOR_UNITS.get(self.variable) if self.variable in VARIABLE_NAME_MAPPING: self._state = getattr(self.device, VARIABLE_NAME_MAPPING[self.variable]) + elif self.variable in PROTECT_SENSOR_TYPES: + # keep backward compatibility + self._state = getattr(self.device, self.variable).capitalize() else: self._state = getattr(self.device, self.variable) -class NestTempSensor(NestSensor): +class NestTempSensor(NestSensorDevice): """Representation of a Nest Temperature sensor.""" @property @@ -195,16 +151,3 @@ class NestTempSensor(NestSensor): self._state = "%s-%s" % (int(low), int(high)) else: self._state = round(temp, 1) - - -class NestProtectSensor(NestSensor): - """Return the state of nest protect.""" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): - """Retrieve latest state.""" - self._state = getattr(self.device, self.variable).capitalize() From 1ac3f0da639c90bc83a2db9cbd357e42380e3c69 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 3 Jun 2018 11:54:03 +0200 Subject: [PATCH 126/137] Ignore the mistaken long_click event of the 86sw (Closes: #14694) (#14785) --- homeassistant/components/binary_sensor/xiaomi_aqara.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 1c0b903d868..72a4cfdfbaa 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -330,6 +330,8 @@ class XiaomiButton(XiaomiBinarySensor): click_type = 'both' elif value == 'shake': click_type = 'shake' + elif value == 'long_click': + return False else: _LOGGER.warning("Unsupported click_type detected: %s", value) return False From 7f59a8ea0c95d4545542f13f2225df2401eb882b Mon Sep 17 00:00:00 2001 From: Jason Woodford Date: Sun, 3 Jun 2018 06:55:49 -0500 Subject: [PATCH 127/137] Update total-connect-client to 0.18 for Honeywell Lynx Touch-Wifi support (#14778) --- 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 1f383e32f92..674eac97f8c 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.17'] +REQUIREMENTS = ['total_connect_client==0.18'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 820a77531df..5836d0f820c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1299,7 +1299,7 @@ todoist-python==7.0.17 toonlib==1.0.2 # homeassistant.components.alarm_control_panel.totalconnect -total_connect_client==0.17 +total_connect_client==0.18 # homeassistant.components.sensor.transmission # homeassistant.components.switch.transmission From 919b431a2459dd36d21cffc498f345a30c1edce7 Mon Sep 17 00:00:00 2001 From: quthla Date: Sun, 3 Jun 2018 15:26:23 +0200 Subject: [PATCH 128/137] Add Kodi OnResume event (#14790) --- homeassistant/components/media_player/kodi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 770d57b5b8e..2322f966eae 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -294,6 +294,7 @@ class KodiDevice(MediaPlayerDevice): # Register notification listeners self._ws_server.Player.OnPause = self.async_on_speed_event self._ws_server.Player.OnPlay = self.async_on_speed_event + self._ws_server.Player.OnResume = self.async_on_speed_event self._ws_server.Player.OnSpeedChanged = self.async_on_speed_event self._ws_server.Player.OnStop = self.async_on_stop self._ws_server.Application.OnVolumeChanged = \ From e35d4beb95a660769eb28dcc4a7b573e6e247ab1 Mon Sep 17 00:00:00 2001 From: quthla Date: Sun, 3 Jun 2018 15:27:17 +0200 Subject: [PATCH 129/137] Fix media_title empty when title is empty but label is set (#14791) --- homeassistant/components/media_player/kodi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 2322f966eae..68a9da55ae4 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -542,8 +542,8 @@ class KodiDevice(MediaPlayerDevice): def media_title(self): """Title of current playing media.""" # find a string we can use as a title - return self._item.get( - 'title', self._item.get('label', self._item.get('file'))) + item = self._item + return item.get('title') or item.get('label') or item.get('file') @property def media_series_title(self): From 70edb2492ac9ac5de34c05e04ed5b684926e7cd1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jun 2018 12:29:57 -0400 Subject: [PATCH 130/137] Version bump to 20180603.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 fca9f33578a..5dad77f64ce 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180601.0'] +REQUIREMENTS = ['home-assistant-frontend==20180603.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 5836d0f820c..fd2bb5b4f5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180601.0 +home-assistant-frontend==20180603.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d870737aac..47f54954cd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180601.0 +home-assistant-frontend==20180603.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 8f696193f08e6642adb4d504f8a2f8052727113b Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Sun, 3 Jun 2018 18:48:51 +0200 Subject: [PATCH 131/137] Add homematicip_cloud illuminance sensor (#14720) * Add iluminance sensor and device_class for sensors * Fix lint --- .../components/sensor/homematicip_cloud.py | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py index aa350f7be5d..ccd1949cc3b 100644 --- a/homeassistant/components/sensor/homematicip_cloud.py +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -10,7 +10,9 @@ import logging from homeassistant.components.homematicip_cloud import ( HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, ATTR_HOME_ID) -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE) _LOGGER = logging.getLogger(__name__) @@ -36,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_devices, """Set up the HomematicIP sensors devices.""" from homematicip.device import ( HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, - TemperatureHumiditySensorDisplay) + TemperatureHumiditySensorDisplay, MotionDetectorIndoor) if discovery_info is None: return @@ -50,6 +52,8 @@ async def async_setup_platform(hass, config, async_add_devices, TemperatureHumiditySensorWithoutDisplay)): devices.append(HomematicipTemperatureSensor(home, device)) devices.append(HomematicipHumiditySensor(home, device)) + if isinstance(device, MotionDetectorIndoor): + devices.append(HomematicipIlluminanceSensor(home, device)) if devices: async_add_devices(devices) @@ -149,6 +153,11 @@ class HomematicipHumiditySensor(HomematicipGenericDevice): """Initialize the thermometer device.""" super().__init__(home, device, 'Humidity') + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_HUMIDITY + @property def icon(self): """Return the icon.""" @@ -172,6 +181,11 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): """Initialize the thermometer device.""" super().__init__(home, device, 'Temperature') + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_TEMPERATURE + @property def icon(self): """Return the icon.""" @@ -186,3 +200,26 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): def unit_of_measurement(self): """Return the unit this state is expressed in.""" return TEMP_CELSIUS + + +class HomematicipIlluminanceSensor(HomematicipGenericDevice): + """MomematicIP the thermometer device.""" + + def __init__(self, home, device): + """Initialize the device.""" + super().__init__(home, device, 'Illuminance') + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_ILLUMINANCE + + @property + def state(self): + """Return the state.""" + return self._device.illumination + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return 'lx' From 2dc40fe16e6ce1373fa6e7ac74139de6e26089fb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jun 2018 16:53:48 -0400 Subject: [PATCH 132/137] Version bump to 0.71.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4c9757b3260..4510d76b136 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 71 -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 3fda97eed7befb5a3e8d0eeafef0bf7f85ab788c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Jun 2018 15:04:28 -0400 Subject: [PATCH 133/137] Bump frontend to 20180608.0b0 --- 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 5dad77f64ce..5892e9136d8 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180603.0'] +REQUIREMENTS = ['home-assistant-frontend==20180608.0b0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index fd2bb5b4f5a..f90f4d8c23b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180603.0 +home-assistant-frontend==20180608.0b0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 47f54954cd2..9c3486d104e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180603.0 +home-assistant-frontend==20180608.0b0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 1cfd770b95c2cb68e6b599c62068740184e62645 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Jun 2018 10:49:54 -0400 Subject: [PATCH 134/137] Use hass iconset (#14185) --- homeassistant/components/config/__init__.py | 2 +- homeassistant/components/hassio/__init__.py | 2 +- homeassistant/components/history.py | 2 +- homeassistant/components/logbook.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 5a8800d9583..b907d4b4217 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -21,7 +21,7 @@ ON_DEMAND = ('zwave',) async def async_setup(hass, config): """Set up the config component.""" await hass.components.frontend.async_register_built_in_panel( - 'config', 'config', 'mdi:settings') + 'config', 'config', 'hass:settings') async def setup_panel(panel_name): """Set up a panel.""" diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 45c35dcdd2a..0fbb2a57ca9 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -178,7 +178,7 @@ def async_setup(hass, config): if 'frontend' in hass.config.components: yield from hass.components.frontend.async_register_built_in_panel( - 'hassio', 'Hass.io', 'mdi:home-assistant') + 'hassio', 'Hass.io', 'hass:home-assistant') if 'http' in config: yield from hassio.update_hass_api(config['http']) diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index c27e394ce28..7ee1c70487f 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -274,7 +274,7 @@ async def async_setup(hass, config): hass.http.register_view(HistoryPeriodView(filters, use_include_order)) await hass.components.frontend.async_register_built_in_panel( - 'history', 'history', 'mdi:poll-box') + 'history', 'history', 'hass:poll-box') return True diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 1ea0b586d33..bcfae533abf 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -100,7 +100,7 @@ async def setup(hass, config): hass.http.register_view(LogbookView(config.get(DOMAIN, {}))) await hass.components.frontend.async_register_built_in_panel( - 'logbook', 'logbook', 'mdi:format-list-bulleted-type') + 'logbook', 'logbook', 'hass:format-list-bulleted-type') hass.services.async_register( DOMAIN, 'log', log_message, schema=LOG_MESSAGE_SCHEMA) From 9a659a5d1d3105c89ef2046dbd1736c25c3e9773 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 7 Jun 2018 23:06:13 +0200 Subject: [PATCH 135/137] Zone - Hass configuration name is optional (#14449) * Hass configuration name is optional * Check explicitly if name is none * Reverted back to old logic for zones configured in configuration.yaml, where many zones can have the same name * New test to verify use case of allowing multiple zones having the same name * Fix too long line --- homeassistant/components/zone/__init__.py | 18 ++++++++---------- tests/components/zone/test_init.py | 19 +++++++++++-------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index d3628fd57f3..c33a16c632e 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -45,27 +45,25 @@ PLATFORM_SCHEMA = vol.Schema({ async def async_setup(hass, config): """Setup configured zones as well as home assistant zone if necessary.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} + hass.data[DOMAIN] = {} + entities = set() zone_entries = configured_zones(hass) for _, entry in config_per_platform(config, DOMAIN): - name = slugify(entry[CONF_NAME]) - if name not in zone_entries: + if slugify(entry[CONF_NAME]) not in zone_entries: zone = Zone(hass, entry[CONF_NAME], entry[CONF_LATITUDE], entry[CONF_LONGITUDE], entry.get(CONF_RADIUS), entry.get(CONF_ICON), entry.get(CONF_PASSIVE)) zone.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, entry[CONF_NAME], None, hass) + ENTITY_ID_FORMAT, entry[CONF_NAME], entities) hass.async_add_job(zone.async_update_ha_state()) - hass.data[DOMAIN][name] = zone + entities.add(zone.entity_id) - if HOME_ZONE not in hass.data[DOMAIN] and HOME_ZONE not in zone_entries: - name = hass.config.location_name - zone = Zone(hass, name, hass.config.latitude, hass.config.longitude, + if ENTITY_ID_HOME not in entities and HOME_ZONE not in zone_entries: + zone = Zone(hass, hass.config.location_name, + hass.config.latitude, hass.config.longitude, DEFAULT_RADIUS, ICON_HOME, False) zone.entity_id = ENTITY_ID_HOME hass.async_add_job(zone.async_update_ha_state()) - hass.data[DOMAIN][slugify(name)] = zone return True diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index 1c698438f2c..c26b3375f3a 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -59,7 +59,6 @@ class TestComponentZone(unittest.TestCase): assert self.hass.config.latitude == state.attributes['latitude'] assert self.hass.config.longitude == state.attributes['longitude'] assert not state.attributes.get('passive', False) - assert 'test_home' in self.hass.data[zone.DOMAIN] def test_setup(self): """Test a successful setup.""" @@ -79,8 +78,6 @@ class TestComponentZone(unittest.TestCase): assert info['longitude'] == state.attributes['longitude'] assert info['radius'] == state.attributes['radius'] assert info['passive'] == state.attributes['passive'] - assert 'test_zone' in self.hass.data[zone.DOMAIN] - assert 'test_home' in self.hass.data[zone.DOMAIN] def test_setup_zone_skips_home_zone(self): """Test that zone named Home should override hass home zone.""" @@ -94,8 +91,17 @@ class TestComponentZone(unittest.TestCase): assert len(self.hass.states.entity_ids('zone')) == 1 state = self.hass.states.get('zone.home') assert info['name'] == state.name - assert 'home' in self.hass.data[zone.DOMAIN] - assert 'test_home' not in self.hass.data[zone.DOMAIN] + + def test_setup_name_can_be_same_on_multiple_zones(self): + """Test that zone named Home should override hass home zone.""" + info = { + 'name': 'Test Zone', + 'latitude': 1.1, + 'longitude': -2.2, + } + assert setup.setup_component( + self.hass, zone.DOMAIN, {'zone': [info, info]}) + assert len(self.hass.states.entity_ids('zone')) == 3 def test_setup_registered_zone_skips_home_zone(self): """Test that config entry named home should override hass home zone.""" @@ -105,7 +111,6 @@ class TestComponentZone(unittest.TestCase): entry.add_to_hass(self.hass) assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': None}) assert len(self.hass.states.entity_ids('zone')) == 0 - assert not self.hass.data[zone.DOMAIN] def test_setup_registered_zone_skips_configured_zone(self): """Test if config entry will override configured zone.""" @@ -123,8 +128,6 @@ class TestComponentZone(unittest.TestCase): assert len(self.hass.states.entity_ids('zone')) == 1 state = self.hass.states.get('zone.test_zone') assert not state - assert 'test_zone' not in self.hass.data[zone.DOMAIN] - assert 'test_home' in self.hass.data[zone.DOMAIN] def test_active_zone_skips_passive_zones(self): """Test active and passive zones.""" From 19a30b0ce615d2bc092307aebf3da8d04b9f3c36 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Jun 2018 15:04:58 -0400 Subject: [PATCH 136/137] Version bump to 0.71.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4510d76b136..4bcc1d86796 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 71 -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 bd1af8c3d86532987b5af111f014ed8bd215020e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Jun 2018 16:57:59 -0400 Subject: [PATCH 137/137] Version bump to 0.71.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4bcc1d86796..552b6392595 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 71 -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)