From 9b0efdc8c896cbb2330094f6f85594ad1f2de24b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 10:51:55 -0400 Subject: [PATCH 001/128] Version bump to 0.73.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5644c3d0a1f..7f315cf616c 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 = 72 +MINOR_VERSION = 73 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 589830771583312d0baa25a20ba8b843510b32ad Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 16 Jun 2018 21:52:03 +0200 Subject: [PATCH 002/128] Bump pyhs100 version (#15001) Fixes #13925 --- homeassistant/components/light/tplink.py | 2 +- homeassistant/components/switch/tplink.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index 4101eab2150..d7544cb6c5a 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -19,7 +19,7 @@ from homeassistant.util.color import \ from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired) -REQUIREMENTS = ['pyHS100==0.3.0'] +REQUIREMENTS = ['pyHS100==0.3.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index cd2a0f189fc..46682d87356 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -14,7 +14,7 @@ from homeassistant.components.switch import ( from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_VOLTAGE) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyHS100==0.3.0'] +REQUIREMENTS = ['pyHS100==0.3.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fdf4dc56f98..dc224ff7454 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -722,7 +722,7 @@ pyCEC==0.4.13 # homeassistant.components.light.tplink # homeassistant.components.switch.tplink -pyHS100==0.3.0 +pyHS100==0.3.1 # homeassistant.components.rfxtrx pyRFXtrx==0.22.1 From 656eae288ebe907137498e3a51e8e0bfcf15eee8 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sat, 16 Jun 2018 22:52:23 +0300 Subject: [PATCH 003/128] Switch to own packaged version of spotipy (#14997) --- homeassistant/components/media_player/spotify.py | 4 +--- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py index 963258f1861..73ec8a175b1 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -20,9 +20,7 @@ from homeassistant.const import ( CONF_NAME, STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv -COMMIT = '544614f4b1d508201d363e84e871f86c90aa26b2' -REQUIREMENTS = ['https://github.com/happyleavesaoc/spotipy/' - 'archive/%s.zip#spotipy==2.4.4' % COMMIT] +REQUIREMENTS = ['spotipy-homeassistant==2.4.4.dev1'] DEPENDENCIES = ['http'] diff --git a/requirements_all.txt b/requirements_all.txt index dc224ff7454..a29caa5cb89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,9 +424,6 @@ httplib2==0.10.3 # homeassistant.components.media_player.braviatv https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7 -# homeassistant.components.media_player.spotify -https://github.com/happyleavesaoc/spotipy/archive/544614f4b1d508201d363e84e871f86c90aa26b2.zip#spotipy==2.4.4 - # homeassistant.components.neato https://github.com/jabesq/pybotvac/archive/v0.0.5.zip#pybotvac==0.0.5 @@ -1281,6 +1278,9 @@ speedtest-cli==2.0.2 # homeassistant.components.sensor.spotcrime spotcrime==1.0.3 +# homeassistant.components.media_player.spotify +spotipy-homeassistant==2.4.4.dev1 + # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql From c5f012c85a8d5f74ff53f00e1641be8e40e9a3fb Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 16 Jun 2018 21:53:25 +0200 Subject: [PATCH 004/128] Remove load power attribute for channel USB (#14996) * Remove load power attribute for channel USB * Fix format --- homeassistant/components/switch/xiaomi_miio.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 1e11b844fdf..37b16f44ea8 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -421,8 +421,11 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): self._device_features = FEATURE_FLAGS_PLUG_V3 self._state_attrs.update({ ATTR_WIFI_LED: None, - ATTR_LOAD_POWER: None, }) + if self._channel_usb is False: + self._state_attrs.update({ + ATTR_LOAD_POWER: None, + }) async def async_turn_on(self, **kwargs): """Turn a channel on.""" @@ -476,7 +479,7 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): if state.wifi_led: self._state_attrs[ATTR_WIFI_LED] = state.wifi_led - if state.load_power: + if self._channel_usb is False and state.load_power: self._state_attrs[ATTR_LOAD_POWER] = state.load_power except DeviceException as ex: From 00cbdffa126278bdd85dccba67e8e3bb38d69908 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 17:12:03 -0400 Subject: [PATCH 005/128] Add experimental UI backend (#15002) * Add experimental UI * Add test * Lint --- homeassistant/components/frontend/__init__.py | 31 ++++++++++++++++--- tests/components/test_frontend.py | 19 ++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0c425ccd3b1..e38376edd9b 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass +from homeassistant.util.yaml import load_yaml REQUIREMENTS = ['home-assistant-frontend==20180615.0'] @@ -105,6 +106,10 @@ SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_GET_TRANSLATIONS, vol.Required('language'): str, }) +WS_TYPE_GET_EXPERIMENTAL_UI = 'frontend/experimental_ui' +SCHEMA_GET_EXPERIMENTAL_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_EXPERIMENTAL_UI, +}) class Panel: @@ -210,6 +215,9 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, SCHEMA_GET_TRANSLATIONS) + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_EXPERIMENTAL_UI, websocket_experimental_config, + SCHEMA_GET_EXPERIMENTAL_UI) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -254,10 +262,11 @@ async def async_setup(hass, config): """Finalize setup of a panel.""" panel.async_register_index_routes(hass.http.app.router, index_view) - await asyncio.wait([ - async_register_built_in_panel(hass, panel) - for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state', - 'dev-template', 'dev-mqtt', 'kiosk')], loop=hass.loop) + await asyncio.wait( + [async_register_built_in_panel(hass, panel) for panel in ( + 'dev-event', 'dev-info', 'dev-service', 'dev-state', + 'dev-template', 'dev-mqtt', 'kiosk', 'experimental-ui')], + loop=hass.loop) hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel @@ -488,3 +497,17 @@ def websocket_get_translations(hass, connection, msg): )) hass.async_add_job(send_translations()) + + +def websocket_experimental_config(hass, connection, msg): + """Send experimental UI config over websocket config.""" + async def send_exp_config(): + """Send experimental frontend config.""" + config = await hass.async_add_job( + load_yaml, hass.config.path('experimental-ui.yaml')) + + connection.send_message_outside(websocket_api.result_message( + msg['id'], config + )) + + hass.async_add_job(send_exp_config()) diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 2f118f24ef0..cb0c72e9edd 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -278,3 +278,22 @@ async def test_get_translations(hass, hass_ws_client): assert msg['type'] == wapi.TYPE_RESULT assert msg['success'] assert msg['result'] == {'resources': {'lang': 'nl'}} + + +async def test_experimental_ui(hass, hass_ws_client): + """Test experimental_ui command.""" + await async_setup_component(hass, 'frontend') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.frontend.load_yaml', + return_value={'hello': 'world'}): + await client.send_json({ + 'id': 5, + 'type': 'frontend/experimental_ui', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] + assert msg['result'] == {'hello': 'world'} From 1375adfeab88bce561a723e62354920a4423c79e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 17:32:49 -0400 Subject: [PATCH 006/128] Bump frontend to 20180616.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 e38376edd9b..0f77b9e0adc 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180615.0'] +REQUIREMENTS = ['home-assistant-frontend==20180616.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index a29caa5cb89..af5e9c6c787 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180615.0 +home-assistant-frontend==20180616.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b32efe9577..03023966d95 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==20180615.0 +home-assistant-frontend==20180616.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 3db5d5bbf96ce410417c4c359634fa3c69c0b386 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jun 2018 22:35:19 -0400 Subject: [PATCH 007/128] Frontend bump to 20180617.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 0f77b9e0adc..25aa0da0a3e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180616.0'] +REQUIREMENTS = ['home-assistant-frontend==20180617.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index af5e9c6c787..d860112c7f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -404,7 +404,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180616.0 +home-assistant-frontend==20180617.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03023966d95..a2245c02cf1 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==20180616.0 +home-assistant-frontend==20180617.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From addca54118fb7f73812d6b51a8d6dfb5d0b9d524 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Sun, 17 Jun 2018 01:13:47 -0400 Subject: [PATCH 008/128] Add entity support to Waze Travel Time (#14934) Current version only supports latitude and longitude or an address for the origin and destination fields. This update allows those fields to use entity IDs of device_tracker, zone, and sensor. --- .../components/sensor/waze_travel_time.py | 185 ++++++++++++------ 1 file changed, 130 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index dbcfcb9cc27..fc40d17d0af 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -7,12 +7,14 @@ https://home-assistant.io/components/sensor.waze_travel_time/ from datetime import timedelta import logging -import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_REGION +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_NAME, CONF_REGION, EVENT_HOMEASSISTANT_START, + ATTR_LATITUDE, ATTR_LONGITUDE) import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.location as location from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -20,6 +22,7 @@ REQUIREMENTS = ['WazeRouteCalculator==0.5'] _LOGGER = logging.getLogger(__name__) +ATTR_DURATION = 'duration' ATTR_DISTANCE = 'distance' ATTR_ROUTE = 'route' @@ -46,6 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_EXCL_FILTER): cv.string, }) +TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone'] + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Waze travel time sensor platform.""" @@ -56,24 +61,46 @@ def setup_platform(hass, config, add_devices, discovery_info=None): incl_filter = config.get(CONF_INCL_FILTER) excl_filter = config.get(CONF_EXCL_FILTER) - try: - waze_data = WazeRouteData( - origin, destination, region, incl_filter, excl_filter) - except requests.exceptions.HTTPError as error: - _LOGGER.error("%s", error) - return + sensor = WazeTravelTime(name, origin, destination, region, + incl_filter, excl_filter) - add_devices([WazeTravelTime(waze_data, name)], True) + add_devices([sensor], True) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, sensor.update) + + +def _get_location_from_attributes(state): + """Get the lat/long string from an states attributes.""" + attr = state.attributes + return '{},{}'.format( + attr.get(ATTR_LATITUDE), + attr.get(ATTR_LONGITUDE) + ) class WazeTravelTime(Entity): """Representation of a Waze travel time sensor.""" - def __init__(self, waze_data, name): + def __init__(self, name, origin, destination, region, + incl_filter, excl_filter): """Initialize the Waze travel time sensor.""" self._name = name + self._region = region + self._incl_filter = incl_filter + self._excl_filter = excl_filter self._state = None - self.waze_data = waze_data + self._origin_entity_id = None + self._destination_entity_id = None + + if origin.split('.', 1)[0] in TRACKABLE_DOMAINS: + self._origin_entity_id = origin + else: + self._origin = origin + + if destination.split('.', 1)[0] in TRACKABLE_DOMAINS: + self._destination_entity_id = destination + else: + self._destination = destination @property def name(self): @@ -83,7 +110,12 @@ class WazeTravelTime(Entity): @property def state(self): """Return the state of the sensor.""" - return round(self._state['duration']) + if self._state is None: + return None + + if 'duration' in self._state: + return round(self._state['duration']) + return None @property def unit_of_measurement(self): @@ -98,54 +130,97 @@ class WazeTravelTime(Entity): @property def device_state_attributes(self): """Return the state attributes of the last update.""" - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_DISTANCE: round(self._state['distance']), - ATTR_ROUTE: self._state['route'], - } + if self._state is None: + return None - def update(self): - """Fetch new state data for the sensor.""" - try: - self.waze_data.update() - self._state = self.waze_data.data - except KeyError: - _LOGGER.error("Error retrieving data from server") + res = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + if 'duration' in self._state: + res[ATTR_DURATION] = self._state['duration'] + if 'distance' in self._state: + res[ATTR_DISTANCE] = self._state['distance'] + if 'route' in self._state: + res[ATTR_ROUTE] = self._state['route'] + return res + def _get_location_from_entity(self, entity_id): + """Get the location from the entity_id.""" + state = self.hass.states.get(entity_id) -class WazeRouteData(object): - """Get data from Waze.""" + if state is None: + _LOGGER.error("Unable to find entity %s", entity_id) + return None - def __init__(self, origin, destination, region, incl_filter, excl_filter): - """Initialize the data object.""" - self._destination = destination - self._origin = origin - self._region = region - self._incl_filter = incl_filter - self._excl_filter = excl_filter - self.data = {} + # Check if the entity has location attributes (zone) + if location.has_location(state): + return _get_location_from_attributes(state) + + # Check if device is in a zone (device_tracker) + zone_state = self.hass.states.get('zone.{}'.format(state.state)) + if location.has_location(zone_state): + _LOGGER.debug( + "%s is in %s, getting zone location", + entity_id, zone_state.entity_id + ) + return _get_location_from_attributes(zone_state) + + # If zone was not found in state then use the state as the location + if entity_id.startswith('sensor.'): + return state.state + + # When everything fails just return nothing + return None + + def _resolve_zone(self, friendly_name): + """Get a lat/long from a zones friendly_name.""" + states = self.hass.states.all() + for state in states: + if state.domain == 'zone' and state.name == friendly_name: + return _get_location_from_attributes(state) + + return friendly_name @Throttle(SCAN_INTERVAL) def update(self): - """Fetch latest data from Waze.""" + """Fetch new state data for the sensor.""" import WazeRouteCalculator - _LOGGER.debug("Update in progress...") - try: - params = WazeRouteCalculator.WazeRouteCalculator( - self._origin, self._destination, self._region, None) - results = params.calc_all_routes_info() - if self._incl_filter is not None: - results = {k: v for k, v in results.items() if - self._incl_filter.lower() in k.lower()} - if self._excl_filter is not None: - results = {k: v for k, v in results.items() if - self._excl_filter.lower() not in k.lower()} - best_route = next(iter(results)) - (duration, distance) = results[best_route] - best_route_str = bytes(best_route, 'ISO-8859-1').decode('UTF-8') - self.data['duration'] = duration - self.data['distance'] = distance - self.data['route'] = best_route_str - except WazeRouteCalculator.WRCError as exp: - _LOGGER.error("Error on retrieving data: %s", exp) - return + + if self._origin_entity_id is not None: + self._origin = self._get_location_from_entity( + self._origin_entity_id + ) + + if self._destination_entity_id is not None: + self._destination = self._get_location_from_entity( + self._destination_entity_id + ) + + self._destination = self._resolve_zone(self._destination) + self._origin = self._resolve_zone(self._origin) + + if self._destination is not None and self._origin is not None: + try: + params = WazeRouteCalculator.WazeRouteCalculator( + self._origin, self._destination, self._region) + routes = params.calc_all_routes_info() + + if self._incl_filter is not None: + routes = {k: v for k, v in routes.items() if + self._incl_filter.lower() in k.lower()} + + if self._excl_filter is not None: + routes = {k: v for k, v in routes.items() if + self._excl_filter.lower() not in k.lower()} + + route = sorted(routes, key=(lambda key: routes[key][0]))[0] + duration, distance = routes[route] + route = bytes(route, 'ISO-8859-1').decode('UTF-8') + self._state = { + 'duration': duration, + 'distance': distance, + 'route': route} + except WazeRouteCalculator.WRCError as exp: + _LOGGER.error("Error on retrieving data: %s", exp) + return + except KeyError: + _LOGGER.error("Error retrieving data from server") + return From 1117371b31083424da2be070d96ac8f56f48e490 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sun, 17 Jun 2018 14:05:25 +0300 Subject: [PATCH 009/128] Switch to own packaged version of anel_pwrctrl (#15011) --- homeassistant/components/switch/anel_pwrctrl.py | 4 +--- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/switch/anel_pwrctrl.py b/homeassistant/components/switch/anel_pwrctrl.py index 9144222e5c7..30739676f17 100644 --- a/homeassistant/components/switch/anel_pwrctrl.py +++ b/homeassistant/components/switch/anel_pwrctrl.py @@ -15,9 +15,7 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_USERNAME) from homeassistant.util import Throttle -REQUIREMENTS = ['https://github.com/mweinelt/anel-pwrctrl/archive/' - 'ed26e8830e28a2bfa4260a9002db23ce3e7e63d7.zip' - '#anel_pwrctrl==0.0.1'] +REQUIREMENTS = ['anel_pwrctrl-homeassistant==0.0.1.dev2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d860112c7f8..fd8332878eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -115,6 +115,9 @@ alpha_vantage==2.0.0 # homeassistant.components.amcrest amcrest==1.2.3 +# homeassistant.components.switch.anel_pwrctrl +anel_pwrctrl-homeassistant==0.0.1.dev2 + # homeassistant.components.media_player.anthemav anthemav==1.1.8 @@ -427,9 +430,6 @@ https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7 # homeassistant.components.neato https://github.com/jabesq/pybotvac/archive/v0.0.5.zip#pybotvac==0.0.5 -# homeassistant.components.switch.anel_pwrctrl -https://github.com/mweinelt/anel-pwrctrl/archive/ed26e8830e28a2bfa4260a9002db23ce3e7e63d7.zip#anel_pwrctrl==0.0.1 - # homeassistant.components.sensor.gtfs https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e2246267dd8.zip#pygtfs==0.1.3 From e3fcf46566c3ef02eb7bf4472b4823e625456998 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sun, 17 Jun 2018 14:07:10 +0300 Subject: [PATCH 010/128] Switch to own packaged version of braviarc (#15009) --- homeassistant/components/media_player/braviatv.py | 4 +--- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index 727bda3be3f..464baed1686 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -18,9 +18,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json -REQUIREMENTS = [ - 'https://github.com/aparraga/braviarc/archive/0.3.7.zip' - '#braviarc==0.3.7'] +REQUIREMENTS = ['braviarc-homeassistant==0.3.7.dev0'] BRAVIA_CONFIG_FILE = 'bravia.conf' diff --git a/requirements_all.txt b/requirements_all.txt index fd8332878eb..783742c17c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -187,6 +187,9 @@ boto3==1.4.7 # homeassistant.scripts.credstash botocore==1.7.34 +# homeassistant.components.media_player.braviatv +braviarc-homeassistant==0.3.7.dev0 + # homeassistant.components.sensor.broadlink # homeassistant.components.switch.broadlink broadlink==0.9.0 @@ -424,9 +427,6 @@ httplib2==0.10.3 # homeassistant.components.sensor.dht # https://github.com/adafruit/Adafruit_Python_DHT/archive/da8cddf7fb629c1ef4f046ca44f42523c9cf2d11.zip#Adafruit_DHT==1.3.2 -# homeassistant.components.media_player.braviatv -https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7 - # homeassistant.components.neato https://github.com/jabesq/pybotvac/archive/v0.0.5.zip#pybotvac==0.0.5 From 722c27f1e2f83e723070e1aff9c826fd78c98f6a Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Sun, 17 Jun 2018 07:37:44 -0400 Subject: [PATCH 011/128] HomeKit style clean up (#14793) --- homeassistant/components/homekit/__init__.py | 4 ++-- homeassistant/components/homekit/type_fans.py | 3 --- .../homekit/type_security_systems.py | 15 +++++++----- .../components/homekit/type_switches.py | 4 ++-- tests/components/homekit/test_accessories.py | 3 +-- .../homekit/test_get_accessories.py | 4 ++-- tests/components/homekit/test_homekit.py | 4 ++-- tests/components/homekit/test_type_covers.py | 2 +- tests/components/homekit/test_type_fans.py | 23 +++++++----------- tests/components/homekit/test_type_lights.py | 6 ++--- .../homekit/test_type_media_players.py | 24 +++++++++---------- tests/components/homekit/test_type_sensors.py | 6 ++--- .../components/homekit/test_type_switches.py | 2 +- .../homekit/test_type_thermostats.py | 4 ++-- tests/components/homekit/test_util.py | 2 +- 15 files changed, 47 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 34372b8b6a8..cb9387fb2c0 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -107,8 +107,8 @@ def get_accessory(hass, driver, state, aid, config): a_type = 'Thermostat' elif state.domain == 'cover': - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) device_class = state.attributes.get(ATTR_DEVICE_CLASS) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if device_class == 'garage' and \ features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): @@ -134,8 +134,8 @@ def get_accessory(hass, driver, state, aid, config): a_type = 'MediaPlayer' elif state.domain == 'sensor': - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) device_class = state.attributes.get(ATTR_DEVICE_CLASS) + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if device_class == DEVICE_CLASS_TEMPERATURE or \ unit in (TEMP_CELSIUS, TEMP_FAHRENHEIT): diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index bf0d4da6a59..aa44b11fefb 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -57,9 +57,6 @@ class Fan(HomeAccessory): def set_state(self, value): """Set state if call came from HomeKit.""" - if self._state == value: - return - _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self._flag[CHAR_ACTIVE] = True service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index bbf8b3f17cb..a7d36720cab 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -5,8 +5,10 @@ from pyhap.const import CATEGORY_ALARM_SYSTEM from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, STATE_ALARM_DISARMED) + ATTR_ENTITY_ID, ATTR_CODE, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_DISARM, 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 @@ -22,10 +24,11 @@ HASS_TO_HOMEKIT = {STATE_ALARM_ARMED_HOME: 0, STATE_ALARM_DISARMED: 3, STATE_ALARM_TRIGGERED: 4} HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} -STATE_TO_SERVICE = {STATE_ALARM_ARMED_HOME: 'alarm_arm_home', - STATE_ALARM_ARMED_AWAY: 'alarm_arm_away', - STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night', - STATE_ALARM_DISARMED: 'alarm_disarm'} +STATE_TO_SERVICE = { + STATE_ALARM_ARMED_AWAY: SERVICE_ALARM_ARM_AWAY, + STATE_ALARM_ARMED_HOME: SERVICE_ALARM_ARM_HOME, + STATE_ALARM_ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT, + STATE_ALARM_DISARMED: SERVICE_ALARM_DISARM} @TYPES.register('SecuritySystem') diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index c8bf8c7ad7c..a5724057eee 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -3,7 +3,7 @@ import logging from pyhap.const import CATEGORY_OUTLET, CATEGORY_SWITCH -from homeassistant.components.switch import DOMAIN as SWITCH +from homeassistant.components.switch import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id @@ -37,7 +37,7 @@ class Outlet(HomeAccessory): 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) + self.hass.services.call(DOMAIN, service, params) def update_state(self, new_state): """Update switch state after state changed.""" diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 711c38443f2..2ffdcb0830f 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -56,8 +56,7 @@ async def test_home_accessory(hass, hk_driver): hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = HomeAccessory(hass, hk_driver, '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 diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 4de68057084..92f8736d1fe 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -34,7 +34,7 @@ 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, None, entity_state, 2, config) is None + assert get_accessory(None, None, entity_state, 2, config) is None # no supported modes for entity entity_state = State('media_player.demo', 'on') @@ -62,7 +62,7 @@ def test_customize_options(config, name): {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', {}, + ('SecuritySystem', 'alarm_control_panel.test', 'armed_away', {}, {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), ('Thermostat', 'climate.test', 'auto', diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 08e8da7857e..cc0370f01b1 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -4,17 +4,17 @@ from unittest.mock import patch, ANY, Mock import pytest from homeassistant import setup -from homeassistant.core import State from homeassistant.components.homekit import ( generate_aid, HomeKit, STATUS_READY, STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( 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 homeassistant.core import State +from homeassistant.helpers.entityfilter import generate_filter from tests.components.homekit.common import patch_debounce diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index c69ddacd328..04ed5df5702 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -19,7 +19,7 @@ def cls(): patcher = patch_debounce() patcher.start() _import = __import__('homeassistant.components.homekit.type_covers', - fromlist=['GarageDoorOpener', 'WindowCovering,', + fromlist=['GarageDoorOpener', 'WindowCovering', 'WindowCoveringBasic']) patcher_tuple = namedtuple('Cls', ['window', 'window_basic', 'garage']) yield patcher_tuple(window=_import.WindowCovering, diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index ba7d4ccdcf0..87a481ff06f 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -5,11 +5,10 @@ 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) + DOMAIN, 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) + STATE_UNKNOWN) from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce @@ -31,8 +30,7 @@ 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}) + hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() acc = cls.fan(hass, hk_driver, 'Fan', entity_id, 2, None) @@ -44,8 +42,7 @@ async def test_fan_basic(hass, hk_driver, cls): await hass.async_block_till_done() assert acc.char_active.value == 1 - hass.states.async_set(entity_id, STATE_OFF, - {ATTR_SUPPORTED_FEATURES: 0}) + hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() assert acc.char_active.value == 0 @@ -58,8 +55,8 @@ async def test_fan_basic(hass, hk_driver, cls): assert acc.char_active.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_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off') await hass.async_add_job(acc.char_active.client_update_value, 1) await hass.async_block_till_done() @@ -97,8 +94,7 @@ async def test_fan_direction(hass, hk_driver, cls): assert acc.char_direction.value == 1 # Set from HomeKit - call_set_direction = async_mock_service(hass, DOMAIN, - SERVICE_SET_DIRECTION) + call_set_direction = async_mock_service(hass, DOMAIN, 'set_direction') await hass.async_add_job(acc.char_direction.client_update_value, 0) await hass.async_block_till_done() @@ -128,13 +124,12 @@ async def test_fan_oscillate(hass, hk_driver, cls): await hass.async_block_till_done() assert acc.char_swing.value == 0 - hass.states.async_set(entity_id, STATE_ON, - {ATTR_OSCILLATING: True}) + hass.states.async_set(entity_id, STATE_ON, {ATTR_OSCILLATING: True}) await hass.async_block_till_done() assert acc.char_swing.value == 1 # Set from HomeKit - call_oscillate = async_mock_service(hass, DOMAIN, SERVICE_OSCILLATE) + call_oscillate = async_mock_service(hass, DOMAIN, 'oscillate') await hass.async_add_job(acc.char_swing.client_update_value, 0) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index a9a5f1c3ece..aab6274f484 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -30,8 +30,7 @@ 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}) + hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() acc = cls.light(hass, hk_driver, 'Light', entity_id, 2, None) @@ -43,8 +42,7 @@ async def test_light_basic(hass, hk_driver, cls): await hass.async_block_till_done() assert acc.char_on.value == 1 - hass.states.async_set(entity_id, STATE_OFF, - {ATTR_SUPPORTED_FEATURES: 0}) + hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() assert acc.char_on.value == 0 diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 4076b1f8a89..681cbba7252 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -1,16 +1,14 @@ """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 ( CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE) +from homeassistant.components.homekit.type_media_players import MediaPlayer +from homeassistant.components.media_player import ( + ATTR_MEDIA_VOLUME_MUTED, DOMAIN) from homeassistant.const import ( - 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) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_IDLE, STATE_OFF, STATE_ON, + STATE_PAUSED, STATE_PLAYING) from tests.common import async_mock_service @@ -59,12 +57,12 @@ async def test_media_player_set_state(hass, hk_driver): assert acc.chars[FEATURE_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) + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off') + call_media_play = async_mock_service(hass, DOMAIN, 'media_play') + call_media_pause = async_mock_service(hass, DOMAIN, 'media_pause') + call_media_stop = async_mock_service(hass, DOMAIN, 'media_stop') + call_toggle_mute = async_mock_service(hass, DOMAIN, 'volume_mute') await hass.async_add_job(acc.chars[FEATURE_ON_OFF] .client_update_value, True) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 54ecbcb196f..901a8e76856 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -14,8 +14,7 @@ async def test_temperature(hass, hk_driver): hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = TemperatureSensor(hass, hk_driver, '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 @@ -70,8 +69,7 @@ async def test_air_quality(hass, hk_driver): hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = AirQualitySensor(hass, hk_driver, '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 diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 3a09d2715d1..c2b80226508 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -1,9 +1,9 @@ """Test different accessory types: Switches.""" import pytest -from homeassistant.core import split_entity_id from homeassistant.components.homekit.type_switches import Outlet, Switch from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.core import split_entity_id 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 00e3e2d22fc..45c340e58c4 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -13,7 +13,7 @@ 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) + CONF_TEMPERATURE_UNIT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce @@ -323,7 +323,7 @@ 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() - with patch.object(hass.config.units, 'temperature_unit', + with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index fa9fddee5fc..9be92b817be 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -2,7 +2,6 @@ import pytest 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, TYPE_OUTLET) @@ -17,6 +16,7 @@ from homeassistant.components.persistent_notification import ( from homeassistant.const import ( ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) +from homeassistant.core import State from tests.common import async_mock_service From c871e8da5d68b7a8445818e1146b8e50806114c1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 17 Jun 2018 15:38:00 +0200 Subject: [PATCH 012/128] Upgrade netdisco to 1.5.0 (#15016) --- homeassistant/components/discovery.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index d7041865892..78b891bae92 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.4.1'] +REQUIREMENTS = ['netdisco==1.5.0'] DOMAIN = 'discovery' diff --git a/requirements_all.txt b/requirements_all.txt index 783742c17c5..d306152a85a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -586,7 +586,7 @@ nanoleaf==0.4.1 netdata==0.1.2 # homeassistant.components.discovery -netdisco==1.4.1 +netdisco==1.5.0 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 From ca2712506bf13ce60d9b16a4921ea802b9749ad1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 17 Jun 2018 15:38:56 +0200 Subject: [PATCH 013/128] Update to hole to 0.3.0 (#15014) --- homeassistant/components/sensor/pi_hole.py | 12 ++++++------ requirements_all.txt | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/pi_hole.py b/homeassistant/components/sensor/pi_hole.py index 8e8c784e68b..2adf5691e2e 100644 --- a/homeassistant/components/sensor/pi_hole.py +++ b/homeassistant/components/sensor/pi_hole.py @@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['pihole==0.1.2'] +REQUIREMENTS = ['hole==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -65,7 +65,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_MONITORED_CONDITIONS, - default=list(MONITORED_CONDITIONS)): + default=['ads_blocked_today']): vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]), }) @@ -73,7 +73,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ async def async_setup_platform( hass, config, async_add_devices, discovery_info=None): """Set up the Pi-hole sensor.""" - from pihole import PiHole + from hole import Hole name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -82,7 +82,7 @@ async def async_setup_platform( verify_tls = config.get(CONF_VERIFY_SSL) session = async_get_clientsession(hass) - pi_hole = PiHoleData(PiHole( + pi_hole = PiHoleData(Hole( host, hass.loop, session, location=location, tls=use_tls, verify_tls=verify_tls)) @@ -164,11 +164,11 @@ class PiHoleData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from the Pi-hole.""" - from pihole.exceptions import PiHoleError + from hole.exceptions import HoleError try: await self.api.get_data() self.available = True - except PiHoleError: + except HoleError: _LOGGER.error("Unable to fetch data from Pi-hole") self.available = False diff --git a/requirements_all.txt b/requirements_all.txt index d306152a85a..ad8a8d5fb2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -406,6 +406,9 @@ hikvision==0.4 # homeassistant.components.notify.hipchat hipnotify==1.0.8 +# homeassistant.components.sensor.pi_hole +hole==0.3.0 + # homeassistant.components.binary_sensor.workday holidays==0.9.5 @@ -651,9 +654,6 @@ pifacedigitalio==3.0.5 # homeassistant.components.light.piglow piglow==1.2.4 -# homeassistant.components.sensor.pi_hole -pihole==0.1.2 - # homeassistant.components.pilight pilight==0.1.1 From bc8093c73ba930bd8f5550da36071bb4e24142c1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 17 Jun 2018 15:39:27 +0200 Subject: [PATCH 014/128] Upgrade numpy to 1.14.5 (#15015) --- homeassistant/components/binary_sensor/trend.py | 2 +- homeassistant/components/image_processing/opencv.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index dcdd312ce81..6a53569798b 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.util import utcnow -REQUIREMENTS = ['numpy==1.14.3'] +REQUIREMENTS = ['numpy==1.14.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index c3e34b4d42b..e01131c7d1b 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -16,7 +16,7 @@ from homeassistant.components.image_processing import ( from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.14.3'] +REQUIREMENTS = ['numpy==1.14.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ad8a8d5fb2a..354a756a849 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -605,7 +605,7 @@ nuheat==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.3 +numpy==1.14.5 # homeassistant.components.google oauth2client==4.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2245c02cf1..b066a52b3c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -99,7 +99,7 @@ mficlient==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.3 +numpy==1.14.5 # homeassistant.components.mqtt # homeassistant.components.shiftr From a74b081d449f2667bf6f6cfe4df18ba0cd6851ac Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 17 Jun 2018 15:41:06 +0200 Subject: [PATCH 015/128] Upgrade youtube_dl to 2018.06.14 (#15013) --- 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 75b90b084fc..497b6f995bd 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.06.02'] +REQUIREMENTS = ['youtube_dl==2018.06.14'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 354a756a849..d6ec886452c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1433,7 +1433,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.06.02 +youtube_dl==2018.06.14 # homeassistant.components.light.zengge zengge==0.2 From 6b908b6f4ee7f5ff0b7f1be286df8ede16bff767 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sun, 17 Jun 2018 16:41:49 +0300 Subject: [PATCH 016/128] Switch nuimo to a hopefully working pypi version (#15006) --- homeassistant/components/nuimo_controller.py | 4 +--- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nuimo_controller.py b/homeassistant/components/nuimo_controller.py index 25e8a230224..e7ab86a5f35 100644 --- a/homeassistant/components/nuimo_controller.py +++ b/homeassistant/components/nuimo_controller.py @@ -15,9 +15,7 @@ from homeassistant.const import (CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP) REQUIREMENTS = [ '--only-binary=all ' # avoid compilation of gattlib - 'https://github.com/getSenic/nuimo-linux-python' - '/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip' - '#nuimo==1.0.0'] + 'nuimo==0.1.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d6ec886452c..b8cbc03b1bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -13,7 +13,7 @@ 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 +--only-binary=all nuimo==0.1.0 # homeassistant.components.sensor.sht31 Adafruit-GPIO==1.0.3 From 3ceee66e1b032528214cf05bbb8dd1c7e4ddd079 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 17 Jun 2018 19:13:39 +0200 Subject: [PATCH 017/128] Remove typing (#15018) --- homeassistant/package_constraints.txt | 1 - requirements_all.txt | 1 - setup.py | 1 - 3 files changed, 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c69e9eb4af4..854fe5c2497 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,6 @@ 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 diff --git a/requirements_all.txt b/requirements_all.txt index b8cbc03b1bf..25c9dd7cd20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -9,7 +9,6 @@ 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 diff --git a/setup.py b/setup.py index a4d15feb7fc..5a171512a7c 100755 --- a/setup.py +++ b/setup.py @@ -52,7 +52,6 @@ REQUIRES = [ 'pytz>=2018.04', 'pyyaml>=3.11,<4', 'requests==2.18.4', - 'typing>=3,<4', 'voluptuous==0.11.1', ] From 40c8f5f70e3527b3230a54917c1adfb05a5b98fa Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 17 Jun 2018 20:34:47 +0200 Subject: [PATCH 018/128] Fix panel URL authentication for Hass.io (#15024) * Update http.py * Update http.py * fix tests * Update test_http.py --- homeassistant/components/hassio/http.py | 2 +- tests/components/hassio/test_http.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index bb4f8219a33..c51d45cc339 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)/.+$'), + re.compile(r'^app/.*$'), re.compile(r'^addons/[^/]*/logo$') } diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index ac90deb9f73..ce260225097 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -47,8 +47,8 @@ def test_auth_required_forward_request(hassio_client): @asyncio.coroutine @pytest.mark.parametrize( 'build_type', [ - 'es5/index.html', 'es5/hassio-app.html', 'latest/index.html', - 'latest/hassio-app.html', 'es5/some-chunk.js', 'es5/app.js', + 'app/index.html', 'app/hassio-app.html', 'app/index.html', + 'app/hassio-app.html', 'app/some-chunk.js', 'app/app.js', ]) def test_forward_request_no_auth_for_panel(hassio_client, build_type): """Test no auth needed for .""" @@ -61,7 +61,7 @@ def test_forward_request_no_auth_for_panel(hassio_client, build_type): '_create_response') as mresp: mresp.return_value = 'response' resp = yield from hassio_client.get( - '/api/hassio/app-{}'.format(build_type)) + '/api/hassio/{}'.format(build_type)) # Check we got right response assert resp.status == 200 From 1533bc1e1f5f58e07e1b9776e144218c728bf79b Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Sun, 17 Jun 2018 14:54:34 -0400 Subject: [PATCH 019/128] Add support for Homekit battery service (#14288) --- .../components/homekit/accessories.py | 51 +++++++++++++++-- homeassistant/components/homekit/const.py | 4 ++ homeassistant/const.py | 1 + tests/components/homekit/test_accessories.py | 55 ++++++++++++++++++- 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 1b0d5ce1be4..d4e6d48c29f 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -8,7 +8,8 @@ from pyhap.accessory import Accessory, Bridge from pyhap.accessory_driver import AccessoryDriver from pyhap.const import CATEGORY_OTHER -from homeassistant.const import __version__ +from homeassistant.const import ( + __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL) from homeassistant.core import callback as ha_callback from homeassistant.core import split_entity_id from homeassistant.helpers.event import ( @@ -16,10 +17,11 @@ from homeassistant.helpers.event import ( from homeassistant.util import dt as dt_util from .const import ( - BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, - DEBOUNCE_TIMEOUT, MANUFACTURER) + BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_BATTERY_LEVEL, + CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, DEBOUNCE_TIMEOUT, + MANUFACTURER, SERV_BATTERY_SERVICE) from .util import ( - show_setup_message, dismiss_setup_message) + convert_to_float, show_setup_message, dismiss_setup_message) _LOGGER = logging.getLogger(__name__) @@ -67,6 +69,23 @@ class HomeAccessory(Accessory): self.entity_id = entity_id self.hass = hass self.debounce = {} + self._support_battery_level = False + self._support_battery_charging = True + + """Add battery service if available""" + battery_level = self.hass.states.get(self.entity_id).attributes \ + .get(ATTR_BATTERY_LEVEL) + if battery_level is None: + return + _LOGGER.debug('%s: Found battery level attribute', self.entity_id) + self._support_battery_level = True + serv_battery = self.add_preload_service(SERV_BATTERY_SERVICE) + self._char_battery = serv_battery.configure_char( + CHAR_BATTERY_LEVEL, value=0) + self._char_charging = serv_battery.configure_char( + CHAR_CHARGING_STATE, value=2) + self._char_low_battery = serv_battery.configure_char( + CHAR_STATUS_LOW_BATTERY, value=0) async def run(self): """Method called by accessory after driver is started. @@ -85,8 +104,32 @@ class HomeAccessory(Accessory): _LOGGER.debug('New_state: %s', new_state) if new_state is None: return + if self._support_battery_level: + self.hass.async_add_job(self.update_battery, new_state) self.hass.async_add_job(self.update_state, new_state) + def update_battery(self, new_state): + """Update battery service if available. + + Only call this function if self._support_battery_level is True. + """ + battery_level = convert_to_float( + new_state.attributes.get(ATTR_BATTERY_LEVEL)) + self._char_battery.set_value(battery_level) + self._char_low_battery.set_value(battery_level < 20) + _LOGGER.debug('%s: Updated battery level to %d', self.entity_id, + battery_level) + if not self._support_battery_charging: + return + charging = new_state.attributes.get(ATTR_BATTERY_CHARGING) + if charging is None: + self._support_battery_charging = False + return + hk_charging = 1 if charging is True else 0 + self._char_charging.set_value(hk_charging) + _LOGGER.debug('%s: Updated battery charging to %d', self.entity_id, + hk_charging) + def update_state(self, new_state): """Method called on state change to update HomeKit value. diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index dec6353850e..33d2c0bfb85 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -38,6 +38,7 @@ TYPE_SWITCH = 'switch' # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' +SERV_BATTERY_SERVICE = 'BatteryService' SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor' SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' SERV_CONTACT_SENSOR = 'ContactSensor' @@ -62,11 +63,13 @@ SERV_WINDOW_COVERING = 'WindowCovering' CHAR_ACTIVE = 'Active' CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_QUALITY = 'AirQuality' +CHAR_BATTERY_LEVEL = 'BatteryLevel' CHAR_BRIGHTNESS = 'Brightness' CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel' CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel' CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected' +CHAR_CHARGING_STATE = 'ChargingState' CHAR_COLOR_TEMPERATURE = 'ColorTemperature' CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' @@ -96,6 +99,7 @@ CHAR_ROTATION_DIRECTION = 'RotationDirection' CHAR_SATURATION = 'Saturation' CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SMOKE_DETECTED = 'SmokeDetected' +CHAR_STATUS_LOW_BATTERY = 'StatusLowBattery' CHAR_SWING_MODE = 'SwingMode' CHAR_TARGET_DOOR_STATE = 'TargetDoorState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' diff --git a/homeassistant/const.py b/homeassistant/const.py index 7f315cf616c..cb6858639f4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -254,6 +254,7 @@ ATTR_DISCOVERED = 'discovered' # Location of the device/sensor ATTR_LOCATION = 'location' +ATTR_BATTERY_CHARGING = 'battery_charging' ATTR_BATTERY_LEVEL = 'battery_level' ATTR_WAKEUP = 'wake_up_interval' diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 2ffdcb0830f..59da90cc75b 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -13,7 +13,9 @@ from homeassistant.components.homekit.const import ( 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 +from homeassistant.const import ( + __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_NOW, + EVENT_TIME_CHANGED) import homeassistant.util.dt as dt_util @@ -88,11 +90,60 @@ async def test_home_accessory(hass, hk_driver): # Test model name from domain entity_id = 'test_model.demo' - acc = HomeAccessory('hass', hk_driver, 'test_name', entity_id, 2, None) + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + 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' +async def test_battery_service(hass, hk_driver): + """Test battery service.""" + entity_id = 'homekit.accessory' + hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 50}) + await hass.async_block_till_done() + + acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, None) + assert acc._char_battery.value == 0 + assert acc._char_low_battery.value == 0 + assert acc._char_charging.value == 2 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc._char_battery.value == 50 + assert acc._char_low_battery.value == 0 + assert acc._char_charging.value == 2 + + hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 15}) + await hass.async_block_till_done() + assert acc._char_battery.value == 15 + assert acc._char_low_battery.value == 1 + assert acc._char_charging.value == 2 + + # Test charging + hass.states.async_set(entity_id, None, { + ATTR_BATTERY_LEVEL: 10, ATTR_BATTERY_CHARGING: True}) + await hass.async_block_till_done() + + acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, None) + assert acc._char_battery.value == 0 + assert acc._char_low_battery.value == 0 + assert acc._char_charging.value == 2 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc._char_battery.value == 10 + assert acc._char_low_battery.value == 1 + assert acc._char_charging.value == 1 + + hass.states.async_set(entity_id, None, { + ATTR_BATTERY_LEVEL: 100, ATTR_BATTERY_CHARGING: False}) + await hass.async_block_till_done() + assert acc._char_battery.value == 100 + assert acc._char_low_battery.value == 0 + assert acc._char_charging.value == 0 + + def test_home_bridge(hk_driver): """Test HomeBridge class.""" bridge = HomeBridge('hass', hk_driver) From 9c17e95fc5e9cc438e4c5a7dd096759c5d54454b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 18 Jun 2018 02:24:11 +0200 Subject: [PATCH 020/128] Upgrade aiohttp to 3.3.2 (#15025) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 854fe5c2497..85f39cfb1b7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,4 +1,4 @@ -aiohttp==3.3.0 +aiohttp==3.3.2 astral==1.6.1 async_timeout==3.0.0 attrs==18.1.0 diff --git a/requirements_all.txt b/requirements_all.txt index 25c9dd7cd20..0afb9d01f44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.3.0 +aiohttp==3.3.2 astral==1.6.1 async_timeout==3.0.0 attrs==18.1.0 diff --git a/setup.py b/setup.py index 5a171512a7c..c2ea6c87cc9 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ - 'aiohttp==3.3.0', + 'aiohttp==3.3.2', 'astral==1.6.1', 'async_timeout==3.0.0', 'attrs==18.1.0', From 33ebd9906823e14115db530b195d9c0b6cb422ea Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Jun 2018 23:03:29 -0400 Subject: [PATCH 021/128] Update translations --- .../components/cast/.translations/ca.json | 15 +++++++++ .../components/cast/.translations/ko.json | 15 +++++++++ .../components/cast/.translations/no.json | 15 +++++++++ .../components/cast/.translations/pl.json | 15 +++++++++ .../components/cast/.translations/ru.json | 15 +++++++++ .../components/cast/.translations/sv.json | 15 +++++++++ .../components/cast/.translations/vi.json | 15 +++++++++ .../cast/.translations/zh-Hans.json | 15 +++++++++ .../components/deconz/.translations/bg.json | 1 + .../components/deconz/.translations/ca.json | 33 +++++++++++++++++++ .../components/deconz/.translations/cs.json | 32 ++++++++++++++++++ .../components/deconz/.translations/en.json | 4 +-- .../components/deconz/.translations/fr.json | 32 ++++++++++++++++++ .../components/deconz/.translations/hu.json | 6 +++- .../components/deconz/.translations/it.json | 26 +++++++++++++++ .../components/deconz/.translations/ko.json | 11 +++++-- .../components/deconz/.translations/lb.json | 6 ++++ .../components/deconz/.translations/no.json | 7 ++++ .../components/deconz/.translations/pl.json | 6 ++++ .../deconz/.translations/pt-BR.json | 32 ++++++++++++++++++ .../components/deconz/.translations/pt.json | 29 ++++++++++++++-- .../components/deconz/.translations/ru.json | 7 ++++ .../components/deconz/.translations/sl.json | 6 ++++ .../components/deconz/.translations/sv.json | 33 +++++++++++++++++++ .../components/deconz/.translations/vi.json | 26 +++++++++++++++ .../deconz/.translations/zh-Hans.json | 7 ++++ .../deconz/.translations/zh-Hant.json | 7 ++++ .../components/hue/.translations/ca.json | 29 ++++++++++++++++ .../components/hue/.translations/cs.json | 29 ++++++++++++++++ .../components/hue/.translations/en.json | 2 +- .../components/hue/.translations/fr.json | 29 ++++++++++++++++ .../components/hue/.translations/hu.json | 3 +- .../components/hue/.translations/it.json | 21 +++++++++++- .../components/hue/.translations/pt-BR.json | 29 ++++++++++++++++ .../components/hue/.translations/pt.json | 24 ++++++++++++++ .../components/hue/.translations/sv.json | 29 ++++++++++++++++ .../components/hue/.translations/vi.json | 17 ++++++++++ .../components/nest/.translations/ca.json | 33 +++++++++++++++++++ .../components/nest/.translations/ko.json | 33 +++++++++++++++++++ .../components/nest/.translations/no.json | 33 +++++++++++++++++++ .../components/nest/.translations/pl.json | 33 +++++++++++++++++++ .../components/nest/.translations/ru.json | 33 +++++++++++++++++++ .../components/nest/.translations/sv.json | 33 +++++++++++++++++++ .../components/nest/.translations/vi.json | 22 +++++++++++++ .../nest/.translations/zh-Hans.json | 33 +++++++++++++++++++ .../sensor/.translations/season.ca.json | 8 +++++ .../sensor/.translations/season.fr.json | 8 +++++ .../sensor/.translations/season.pt-BR.json | 8 +++++ .../components/sonos/.translations/ca.json | 15 +++++++++ .../components/sonos/.translations/ko.json | 15 +++++++++ .../components/sonos/.translations/no.json | 15 +++++++++ .../components/sonos/.translations/pl.json | 15 +++++++++ .../components/sonos/.translations/ru.json | 15 +++++++++ .../components/sonos/.translations/sv.json | 15 +++++++++ .../components/sonos/.translations/vi.json | 15 +++++++++ .../sonos/.translations/zh-Hans.json | 15 +++++++++ .../components/zone/.translations/bg.json | 21 ++++++++++++ .../components/zone/.translations/ca.json | 21 ++++++++++++ .../components/zone/.translations/cs.json | 21 ++++++++++++ .../components/zone/.translations/fr.json | 21 ++++++++++++ .../components/zone/.translations/hu.json | 21 ++++++++++++ .../components/zone/.translations/it.json | 21 ++++++++++++ .../components/zone/.translations/ko.json | 2 +- .../components/zone/.translations/pt-BR.json | 21 ++++++++++++ .../components/zone/.translations/pt.json | 3 +- .../components/zone/.translations/sl.json | 21 ++++++++++++ .../components/zone/.translations/sv.json | 21 ++++++++++++ .../components/zone/.translations/vi.json | 21 ++++++++++++ .../zone/.translations/zh-Hant.json | 21 ++++++++++++ homeassistant/config_entries.py | 3 ++ 70 files changed, 1267 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/cast/.translations/ca.json create mode 100644 homeassistant/components/cast/.translations/ko.json create mode 100644 homeassistant/components/cast/.translations/no.json create mode 100644 homeassistant/components/cast/.translations/pl.json create mode 100644 homeassistant/components/cast/.translations/ru.json create mode 100644 homeassistant/components/cast/.translations/sv.json create mode 100644 homeassistant/components/cast/.translations/vi.json create mode 100644 homeassistant/components/cast/.translations/zh-Hans.json create mode 100644 homeassistant/components/deconz/.translations/ca.json create mode 100644 homeassistant/components/deconz/.translations/cs.json create mode 100644 homeassistant/components/deconz/.translations/fr.json create mode 100644 homeassistant/components/deconz/.translations/it.json create mode 100644 homeassistant/components/deconz/.translations/pt-BR.json create mode 100644 homeassistant/components/deconz/.translations/sv.json create mode 100644 homeassistant/components/deconz/.translations/vi.json create mode 100644 homeassistant/components/hue/.translations/ca.json create mode 100644 homeassistant/components/hue/.translations/cs.json create mode 100644 homeassistant/components/hue/.translations/fr.json create mode 100644 homeassistant/components/hue/.translations/pt-BR.json create mode 100644 homeassistant/components/hue/.translations/sv.json create mode 100644 homeassistant/components/hue/.translations/vi.json create mode 100644 homeassistant/components/nest/.translations/ca.json create mode 100644 homeassistant/components/nest/.translations/ko.json create mode 100644 homeassistant/components/nest/.translations/no.json create mode 100644 homeassistant/components/nest/.translations/pl.json create mode 100644 homeassistant/components/nest/.translations/ru.json create mode 100644 homeassistant/components/nest/.translations/sv.json create mode 100644 homeassistant/components/nest/.translations/vi.json create mode 100644 homeassistant/components/nest/.translations/zh-Hans.json create mode 100644 homeassistant/components/sensor/.translations/season.ca.json create mode 100644 homeassistant/components/sensor/.translations/season.fr.json create mode 100644 homeassistant/components/sensor/.translations/season.pt-BR.json create mode 100644 homeassistant/components/sonos/.translations/ca.json create mode 100644 homeassistant/components/sonos/.translations/ko.json create mode 100644 homeassistant/components/sonos/.translations/no.json create mode 100644 homeassistant/components/sonos/.translations/pl.json create mode 100644 homeassistant/components/sonos/.translations/ru.json create mode 100644 homeassistant/components/sonos/.translations/sv.json create mode 100644 homeassistant/components/sonos/.translations/vi.json create mode 100644 homeassistant/components/sonos/.translations/zh-Hans.json create mode 100644 homeassistant/components/zone/.translations/bg.json create mode 100644 homeassistant/components/zone/.translations/ca.json create mode 100644 homeassistant/components/zone/.translations/cs.json create mode 100644 homeassistant/components/zone/.translations/fr.json create mode 100644 homeassistant/components/zone/.translations/hu.json create mode 100644 homeassistant/components/zone/.translations/it.json create mode 100644 homeassistant/components/zone/.translations/pt-BR.json create mode 100644 homeassistant/components/zone/.translations/sl.json create mode 100644 homeassistant/components/zone/.translations/sv.json create mode 100644 homeassistant/components/zone/.translations/vi.json create mode 100644 homeassistant/components/zone/.translations/zh-Hant.json diff --git a/homeassistant/components/cast/.translations/ca.json b/homeassistant/components/cast/.translations/ca.json new file mode 100644 index 00000000000..e65e00f8624 --- /dev/null +++ b/homeassistant/components/cast/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius de Google Cast a la xarxa.", + "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de Google Cast." + }, + "step": { + "confirm": { + "description": "Voleu configurar Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json new file mode 100644 index 00000000000..2be2a69c171 --- /dev/null +++ b/homeassistant/components/cast/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Googgle Cast \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "Google Cast\uc758 \ub2e8\uc77c \uad6c\uc131 \ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "Google Cast\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/no.json b/homeassistant/components/cast/.translations/no.json new file mode 100644 index 00000000000..d36c929e721 --- /dev/null +++ b/homeassistant/components/cast/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen Google Cast enheter funnet p\u00e5 nettverket.", + "single_instance_allowed": "Kun en enkelt konfigurasjon av Google Cast er n\u00f8dvendig." + }, + "step": { + "confirm": { + "description": "\u00d8nsker du \u00e5 sette opp Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/pl.json b/homeassistant/components/cast/.translations/pl.json new file mode 100644 index 00000000000..c4399f95def --- /dev/null +++ b/homeassistant/components/cast/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 Google Cast.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja Google Cast." + }, + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/ru.json b/homeassistant/components/cast/.translations/ru.json new file mode 100644 index 00000000000..9c9353da37e --- /dev/null +++ b/homeassistant/components/cast/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Google Cast \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Google Cast." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/sv.json b/homeassistant/components/cast/.translations/sv.json new file mode 100644 index 00000000000..aea55058d10 --- /dev/null +++ b/homeassistant/components/cast/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga Google Cast-enheter hittades i n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration av Google Cast \u00e4r n\u00f6dv\u00e4ndig." + }, + "step": { + "confirm": { + "description": "Vill du konfigurera Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/vi.json b/homeassistant/components/cast/.translations/vi.json new file mode 100644 index 00000000000..2f2982293cf --- /dev/null +++ b/homeassistant/components/cast/.translations/vi.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Kh\u00f4ng t\u00ecm th\u1ea5y thi\u1ebft b\u1ecb Google Cast n\u00e0o tr\u00ean m\u1ea1ng.", + "single_instance_allowed": "Ch\u1ec9 c\u1ea7n m\u1ed9t c\u1ea5u h\u00ecnh duy nh\u1ea5t c\u1ee7a Google Cast l\u00e0 \u0111\u1ee7." + }, + "step": { + "confirm": { + "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp Google Cast kh\u00f4ng?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/zh-Hans.json b/homeassistant/components/cast/.translations/zh-Hans.json new file mode 100644 index 00000000000..4a844d3d4dd --- /dev/null +++ b/homeassistant/components/cast/.translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 Google Cast \u8bbe\u5907\u3002", + "single_instance_allowed": "\u53ea\u6709\u4e00\u6b21 Google Cast \u914d\u7f6e\u662f\u5fc5\u8981\u7684\u3002" + }, + "step": { + "confirm": { + "description": "\u60a8\u60f3\u8981\u914d\u7f6e Google Cast \u5417\uff1f", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index 91727cae257..2ea65762063 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u041c\u043e\u0441\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043c\u043e\u0441\u0442\u043e\u0432\u0435 deCONZ", "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 deCONZ" }, diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json new file mode 100644 index 00000000000..0a9e6fdee3f --- /dev/null +++ b/homeassistant/components/deconz/.translations/ca.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat", + "no_bridges": "No s'han descobert enlla\u00e7os amb deCONZ", + "one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia deCONZ" + }, + "error": { + "no_key": "No s'ha pogut obtenir una clau API" + }, + "step": { + "init": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port (predeterminat: '80')" + }, + "title": "Definiu la passarel\u00b7la deCONZ" + }, + "link": { + "description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ\n2. Prem el bot\u00f3 \"Desbloquejar passarel\u00b7la\"", + "title": "Vincular amb deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Permet la importaci\u00f3 de sensors virtuals", + "allow_deconz_groups": "Permet la importaci\u00f3 de grups deCONZ" + }, + "title": "Opcions de configuraci\u00f3 addicionals per deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json new file mode 100644 index 00000000000..0721cac3321 --- /dev/null +++ b/homeassistant/components/deconz/.translations/cs.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "P\u0159emost\u011bn\u00ed je ji\u017e nakonfigurov\u00e1no", + "no_bridges": "\u017d\u00e1dn\u00e9 deCONZ p\u0159emost\u011bn\u00ed nebyly nalezeny", + "one_instance_only": "Komponent podporuje pouze jednu instanci deCONZ" + }, + "error": { + "no_key": "Nelze z\u00edskat kl\u00ed\u010d API" + }, + "step": { + "init": { + "data": { + "host": "Hostitel", + "port": "Port (v\u00fdchoz\u00ed hodnota: '80')" + }, + "title": "Definujte br\u00e1nu deCONZ" + }, + "link": { + "description": "Odemkn\u011bte br\u00e1nu deCONZ, pro registraci v Home Assistant. \n\n 1. P\u0159ejd\u011bte do nastaven\u00ed syst\u00e9mu deCONZ \n 2. Stiskn\u011bte tla\u010d\u00edtko \"Unlock Gateway\"", + "title": "Propojit s deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel" + }, + "title": "Dal\u0161\u00ed mo\u017enosti konfigurace pro deCONZ" + } + }, + "title": "Br\u00e1na deCONZ Zigbee" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 465c6c1e0e8..f55f64ca430 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -21,11 +21,11 @@ "title": "Link with deCONZ" }, "options": { - "title": "Extra configuration options for deCONZ", "data": { "allow_clip_sensor": "Allow importing virtual sensors", "allow_deconz_groups": "Allow importing deCONZ groups" - } + }, + "title": "Extra configuration options for deCONZ" } }, "title": "deCONZ Zigbee gateway" diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json new file mode 100644 index 00000000000..02f174cd59f --- /dev/null +++ b/homeassistant/components/deconz/.translations/fr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9", + "no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert", + "one_instance_only": "Le composant prend uniquement en charge une instance deCONZ" + }, + "error": { + "no_key": "Impossible d'obtenir une cl\u00e9 d'API" + }, + "step": { + "init": { + "data": { + "host": "H\u00f4te", + "port": "Port (valeur par d\u00e9faut : 80)" + }, + "title": "Initialiser la passerelle deCONZ" + }, + "link": { + "description": "D\u00e9verrouillez votre passerelle deCONZ pour vous enregistrer aupr\u00e8s de Home Assistant. \n\n 1. Acc\u00e9dez aux param\u00e8tres du syst\u00e8me deCONZ \n 2. Cliquez sur \"D\u00e9verrouiller la passerelle\"", + "title": "Lien vers deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Autoriser l'importation de capteurs virtuels" + }, + "title": "Options de configuration suppl\u00e9mentaires pour deCONZ" + } + }, + "title": "Passerelle deCONZ Zigbee" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index 42aab9c6d7e..c1fd76c5035 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van", + "no_bridges": "Nem tal\u00e1ltam deCONZ bridget", "one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat" }, "error": { @@ -11,9 +13,11 @@ "data": { "host": "H\u00e1zigazda (Host)", "port": "Port (alap\u00e9rtelmezett \u00e9rt\u00e9k: '80')" - } + }, + "title": "deCONZ \u00e1tj\u00e1r\u00f3 megad\u00e1sa" }, "link": { + "description": "Oldja fel a deCONZ \u00e1tj\u00e1r\u00f3t a Home Assistant-ban val\u00f3 regisztr\u00e1l\u00e1shoz.\n\n1. Menjen a deCONZ rendszer be\u00e1ll\u00edt\u00e1sokhoz\n2. Nyomja meg az \"\u00c1tj\u00e1r\u00f3 felold\u00e1sa\" gombot", "title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz" } }, diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json new file mode 100644 index 00000000000..6fc7158b882 --- /dev/null +++ b/homeassistant/components/deconz/.translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato", + "no_bridges": "Nessun bridge deCONZ rilevato", + "one_instance_only": "Il componente supporto solo un'istanza di deCONZ" + }, + "error": { + "no_key": "Impossibile ottenere una API key" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Porta (valore di default: '80')" + }, + "title": "Definisci il gateway deCONZ" + }, + "link": { + "description": "Sblocca il tuo gateway deCONZ per registrarlo in Home Assistant.\n\n1. Vai nelle impostazioni di sistema di deCONZ\n2. Premi il bottone \"Unlock Gateway\"", + "title": "Collega con deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index d6de1028218..9c5ffa19257 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -18,9 +18,16 @@ }, "link": { "description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Unlock Gateway\" \ubc84\ud2bc\uc744 \ub204\ub974\uc138\uc694 ", - "title": "deCONZ \uc640 \uc5f0\uacb0" + "title": "deCONZ\uc640 \uc5f0\uacb0" + }, + "options": { + "data": { + "allow_clip_sensor": "\uac00\uc0c1 \uc13c\uc11c \uac00\uc838\uc624\uae30 \ud5c8\uc6a9", + "allow_deconz_groups": "deCONZ \ub0b4\uc6a9 \uac00\uc838\uc624\uae30 \ud5c8\uc6a9" + }, + "title": "deCONZ\ub97c \uc704\ud55c \ucd94\uac00 \uad6c\uc131 \uc635\uc158" } }, - "title": "deCONZ" + "title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 2a9dfc5e543..46190d23926 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -19,6 +19,12 @@ "link": { "description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op\u00a0deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen", "title": "Link mat deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren" + }, + "title": "Extra Konfiguratiouns Optiounen fir deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 25e3b0b7d68..55518b7da53 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -19,6 +19,13 @@ "link": { "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen", "title": "Koble til deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Tillat import av virtuelle sensorer", + "allow_deconz_groups": "Tillat import av deCONZ grupper" + }, + "title": "Ekstra konfigurasjonsalternativer for deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index bb7488fcbec..461e8b185ee 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -19,6 +19,12 @@ "link": { "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawie\u0144 systemu deCONZ \n 2. Naci\u015bnij przycisk \"Odblokuj bramk\u0119\"", "title": "Po\u0142\u0105cz z deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Zezwalaj na importowanie wirtualnych sensor\u00f3w" + }, + "title": "Dodatkowe opcje konfiguracji dla deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/pt-BR.json b/homeassistant/components/deconz/.translations/pt-BR.json new file mode 100644 index 00000000000..065c51aee21 --- /dev/null +++ b/homeassistant/components/deconz/.translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "A ponte j\u00e1 est\u00e1 configurada", + "no_bridges": "N\u00e3o h\u00e1 pontes de deCONZ descobertas", + "one_instance_only": "Componente suporta apenas uma inst\u00e2ncia deCONZ" + }, + "error": { + "no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API" + }, + "step": { + "init": { + "data": { + "host": "Hospedeiro", + "port": "Porta (valor padr\u00e3o: '80')" + }, + "title": "Defina o gateway deCONZ" + }, + "link": { + "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", + "title": "Linkar com deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais" + }, + "title": "Op\u00e7\u00f5es extras de configura\u00e7\u00e3o para deCONZ" + } + }, + "title": "Gateway deCONZ Zigbee" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json index 2a00c698691..6ccbfe9f217 100644 --- a/homeassistant/components/deconz/.translations/pt.json +++ b/homeassistant/components/deconz/.translations/pt.json @@ -1,7 +1,32 @@ { "config": { "abort": { - "already_configured": "Bridge j\u00e1 est\u00e1 configurada" - } + "already_configured": "Bridge j\u00e1 est\u00e1 configurada", + "no_bridges": "Nenhum deCONZ descoberto", + "one_instance_only": "Componente suporta apenas uma conex\u00e3o deCONZ" + }, + "error": { + "no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API" + }, + "step": { + "init": { + "data": { + "host": "Servidor", + "port": "Porta (por omiss\u00e3o: '80')" + }, + "title": "Defina o gateway deCONZ" + }, + "link": { + "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", + "title": "Link com deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais" + }, + "title": "Op\u00e7\u00f5es extra de configura\u00e7\u00e3o para deCONZ" + } + }, + "title": "deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index b0dc6a8a4a8..56490f67cb3 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -19,6 +19,13 @@ "link": { "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437\u00bb", "title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432", + "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0433\u0440\u0443\u043f\u043f deCONZ" + }, + "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index b738002b273..59c5577c96b 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -19,6 +19,12 @@ "link": { "description": "Odklenite va\u0161 deCONZ gateway za registracijo z Home Assistant-om. \n1. Pojdite v deCONT sistemske nastavitve\n2. Pritisnite tipko \"odkleni prehod\"", "title": "Povezava z deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev" + }, + "title": "Dodatne mo\u017enosti konfiguracije za deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json new file mode 100644 index 00000000000..88cf8742acd --- /dev/null +++ b/homeassistant/components/deconz/.translations/sv.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Bryggan \u00e4r redan konfigurerad", + "no_bridges": "Inga deCONZ-bryggor uppt\u00e4cktes", + "one_instance_only": "Komponenten st\u00f6djer endast en deCONZ-instans" + }, + "error": { + "no_key": "Det gick inte att ta emot en API-nyckel" + }, + "step": { + "init": { + "data": { + "host": "V\u00e4rd", + "port": "Port (standardv\u00e4rde: '80')" + }, + "title": "Definiera deCONZ-gatewaye" + }, + "link": { + "description": "L\u00e5s upp din deCONZ-gateway f\u00f6r att registrera dig med Home Assistant. \n\n 1. G\u00e5 till deCONZ-systeminst\u00e4llningarna \n 2. Tryck p\u00e5 \"L\u00e5s upp gateway\"-knappen", + "title": "L\u00e4nka med deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Till\u00e5t import av virtuella sensorer", + "allow_deconz_groups": "Till\u00e5t import av deCONZ-grupper" + }, + "title": "Extra konfigurationsalternativ f\u00f6r deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/vi.json b/homeassistant/components/deconz/.translations/vi.json new file mode 100644 index 00000000000..00f1d9be57f --- /dev/null +++ b/homeassistant/components/deconz/.translations/vi.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "C\u1ea7u \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1ea5u h\u00ecnh", + "no_bridges": "Kh\u00f4ng t\u00ecm th\u1ea5y c\u1ea7u deCONZ n\u00e0o", + "one_instance_only": "Th\u00e0nh ph\u1ea7n ch\u1ec9 h\u1ed7 tr\u1ee3 m\u1ed9t c\u00e1 th\u1ec3 deCONZ" + }, + "error": { + "no_key": "Kh\u00f4ng th\u1ec3 l\u1ea5y kh\u00f3a API" + }, + "step": { + "init": { + "data": { + "port": "C\u1ed5ng (gi\u00e1 tr\u1ecb m\u1eb7c \u0111\u1ecbnh: '80')" + } + }, + "options": { + "data": { + "allow_clip_sensor": "Cho ph\u00e9p nh\u1eadp c\u1ea3m bi\u1ebfn \u1ea3o", + "allow_deconz_groups": "Cho ph\u00e9p nh\u1eadp c\u00e1c nh\u00f3m deCONZ" + }, + "title": "T\u00f9y ch\u1ecdn c\u1ea5u h\u00ecnh b\u1ed5 sung cho deCONZ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/zh-Hans.json b/homeassistant/components/deconz/.translations/zh-Hans.json index f41b5b5111c..2e5a216c77d 100644 --- a/homeassistant/components/deconz/.translations/zh-Hans.json +++ b/homeassistant/components/deconz/.translations/zh-Hans.json @@ -19,6 +19,13 @@ "link": { "description": "\u89e3\u9501\u60a8\u7684 deCONZ \u7f51\u5173\u4ee5\u6ce8\u518c\u5230 Home Assistant\u3002 \n\n 1. \u524d\u5f80 deCONZ \u7cfb\u7edf\u8bbe\u7f6e\n 2. \u70b9\u51fb\u201c\u89e3\u9501\u7f51\u5173\u201d\u6309\u94ae", "title": "\u8fde\u63a5 deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "\u5141\u8bb8\u5bfc\u5165\u865a\u62df\u4f20\u611f\u5668", + "allow_deconz_groups": "\u5141\u8bb8\u5bfc\u5165 deCONZ \u7fa4\u7ec4" + }, + "title": "deCONZ \u7684\u9644\u52a0\u914d\u7f6e\u9879" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 33be3846eb8..17cbe87f1e8 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u5be6\u4f8b" }, @@ -18,6 +19,12 @@ "link": { "description": "\u89e3\u9664 deCONZ \u7db2\u95dc\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u89e3\u9664\u7db2\u95dc\u9396\u5b9a\uff08Unlock Gateway\uff09\u300d\u6309\u9215", "title": "\u9023\u7d50\u81f3 deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668" + }, + "title": "deCONZ \u9644\u52a0\u8a2d\u5b9a\u9078\u9805" } }, "title": "deCONZ" diff --git a/homeassistant/components/hue/.translations/ca.json b/homeassistant/components/hue/.translations/ca.json new file mode 100644 index 00000000000..6c41eed5467 --- /dev/null +++ b/homeassistant/components/hue/.translations/ca.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Tots els enlla\u00e7os Philips Hue ja estan configurats", + "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat", + "cannot_connect": "No es pot connectar amb l'enlla\u00e7", + "discover_timeout": "No s'han pogut descobrir enlla\u00e7os Hue", + "no_bridges": "No s'han trobat enlla\u00e7os Philips Hue", + "unknown": "S'ha produ\u00eft un error desconegut" + }, + "error": { + "linking": "S'ha produ\u00eft un error desconegut al vincular.", + "register_failed": "No s'ha pogut registrar, torneu-ho a provar" + }, + "step": { + "init": { + "data": { + "host": "Amfitri\u00f3" + }, + "title": "Tria l'enlla\u00e7 Hue" + }, + "link": { + "description": "Premeu el bot\u00f3 de l'ella\u00e7 per registrar Philips Hue amb Home Assistant. \n\n ![Ubicaci\u00f3 del bot\u00f3 al pont](/static/images/config_philips_hue.jpg)", + "title": "Vincular concentrador" + } + }, + "title": "Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/cs.json b/homeassistant/components/hue/.translations/cs.json new file mode 100644 index 00000000000..35c423b1a03 --- /dev/null +++ b/homeassistant/components/hue/.translations/cs.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "V\u0161echny Philips Hue p\u0159emost\u011bn\u00ed jsou ji\u017e nakonfigurov\u00e1ny", + "already_configured": "P\u0159emost\u011bn\u00ed je ji\u017e nakonfigurov\u00e1no", + "cannot_connect": "Nelze se p\u0159ipojit k p\u0159emost\u011bn\u00ed", + "discover_timeout": "Nelze nal\u00e9zt p\u0159emost\u011bn\u00ed Hue", + "no_bridges": "Nebyly nalezeny \u017e\u00e1dn\u00e9 p\u0159emost\u011bn\u00ed Philips Hue", + "unknown": "Do\u0161lo k nezn\u00e1m\u00e9 chyb\u011b" + }, + "error": { + "linking": "Do\u0161lo k nezn\u00e1m\u00e9 chyb\u011b propojen\u00ed.", + "register_failed": "Registrace se nezda\u0159ila, zkuste to pros\u00edm znovu" + }, + "step": { + "init": { + "data": { + "host": "Hostitel" + }, + "title": "Vybrat Hue p\u0159emost\u011bn\u00ed" + }, + "link": { + "description": "Stiskn\u011bte tla\u010d\u00edtko na p\u0159emost\u011bn\u00ed k registraci Philips Hue v Home Assistant.\n\n! [Um\u00edst\u011bn\u00ed tla\u010d\u00edtka na p\u0159emost\u011bn\u00ed] (/ static/images/config_philips_hue.jpg)", + "title": "P\u0159ipojit Hub" + } + }, + "title": "Philips Hue p\u0159emost\u011bn\u00ed" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json index b0459ec3916..cea8d8be10a 100644 --- a/homeassistant/components/hue/.translations/en.json +++ b/homeassistant/components/hue/.translations/en.json @@ -24,6 +24,6 @@ "title": "Link Hub" } }, - "title": "Philips Hue Bridge" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/fr.json b/homeassistant/components/hue/.translations/fr.json new file mode 100644 index 00000000000..73613f237da --- /dev/null +++ b/homeassistant/components/hue/.translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Tous les ponts Philips Hue sont d\u00e9j\u00e0 configur\u00e9s", + "already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "Connexion au pont impossible", + "discover_timeout": "D\u00e9tection de ponts Philips Hue impossible", + "no_bridges": "Aucun pont Philips Hue n'a \u00e9t\u00e9 d\u00e9couvert", + "unknown": "Une erreur inconnue s'est produite" + }, + "error": { + "linking": "Une erreur inconnue s'est produite lors de la liaison entre le pont et Home Assistant", + "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer." + }, + "step": { + "init": { + "data": { + "host": "H\u00f4te" + }, + "title": "Choisissez le pont Philips Hue" + }, + "link": { + "description": "Appuyez sur le bouton du pont pour lier Philips Hue avec Home Assistant. \n\n ![Emplacement du bouton sur le pont] (/static/images/config_philips_hue.jpg)", + "title": "Hub de liaison" + } + }, + "title": "Pont Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/hu.json b/homeassistant/components/hue/.translations/hu.json index a4032dcbcfc..be6548f59a0 100644 --- a/homeassistant/components/hue/.translations/hu.json +++ b/homeassistant/components/hue/.translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt", - "already_configured": "A bridge m\u00e1r konfigur\u00e1lt", + "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Nem siker\u00fclt csatlakozni a bridge-hez.", "discover_timeout": "Nem tal\u00e1ltam a Hue bridget", "no_bridges": "Nem tal\u00e1ltam Philips Hue bridget", @@ -20,6 +20,7 @@ "title": "V\u00e1lassz Hue bridge-t" }, "link": { + "description": "Nyomja meg a gombot a bridge-en a Philips Hue Home Assistant-ben val\u00f3 regisztr\u00e1l\u00e1s\u00e1hoz.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", "title": "Kapcsol\u00f3d\u00e1s a hubhoz" } }, diff --git a/homeassistant/components/hue/.translations/it.json b/homeassistant/components/hue/.translations/it.json index 2c7a8c1924d..a9f2a732127 100644 --- a/homeassistant/components/hue/.translations/it.json +++ b/homeassistant/components/hue/.translations/it.json @@ -2,8 +2,27 @@ "config": { "abort": { "all_configured": "Tutti i bridge Philips Hue sono gi\u00e0 configurati", + "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi al bridge", "discover_timeout": "Impossibile trovare i bridge Hue", - "no_bridges": "Nessun bridge Hue di Philips trovato" + "no_bridges": "Nessun bridge Hue di Philips trovato", + "unknown": "Si \u00e8 verificato un errore" + }, + "error": { + "linking": "Si \u00e8 verificato un errore sconosciuto in fase di collegamento.", + "register_failed": "Errore in fase di registrazione, riprova" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Selezione il bridge Hue" + }, + "link": { + "description": "Premi il pulsante sul bridge per registrare Philips Hue con Home Assistant\n\n![Posizione del pulsante sul bridge](/static/images/config_philips_hue.jpg)", + "title": "Collega Hub" + } }, "title": "Philips Hue Bridge" } diff --git a/homeassistant/components/hue/.translations/pt-BR.json b/homeassistant/components/hue/.translations/pt-BR.json new file mode 100644 index 00000000000..5c6e409245c --- /dev/null +++ b/homeassistant/components/hue/.translations/pt-BR.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Todas as pontes Philips Hue j\u00e1 est\u00e3o configuradas", + "already_configured": "A ponte j\u00e1 est\u00e1 configurada", + "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se \u00e0 ponte", + "discover_timeout": "Incapaz de descobrir pontes Hue", + "no_bridges": "N\u00e3o h\u00e1 pontes Philips Hue descobertas", + "unknown": "Ocorreu um erro desconhecido" + }, + "error": { + "linking": "Ocorreu um erro de liga\u00e7\u00e3o desconhecido.", + "register_failed": "Falhou ao registrar, por favor tente novamente" + }, + "step": { + "init": { + "data": { + "host": "Hospedeiro" + }, + "title": "Escolha a ponte Hue" + }, + "link": { + "description": "Pressione o bot\u00e3o na ponte para registrar o Philips Hue com o Home Assistant. \n\n ![Localiza\u00e7\u00e3o do bot\u00e3o na ponte] (/static/images/config_philips_hue.jpg)", + "title": "Hub de links" + } + }, + "title": "Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/pt.json b/homeassistant/components/hue/.translations/pt.json index 8c4c45f9c89..f7988d82d8c 100644 --- a/homeassistant/components/hue/.translations/pt.json +++ b/homeassistant/components/hue/.translations/pt.json @@ -1,5 +1,29 @@ { "config": { + "abort": { + "all_configured": "Todas os Philips Hue j\u00e1 est\u00e3o configuradas", + "already_configured": "Hue j\u00e1 est\u00e1 configurado", + "cannot_connect": "N\u00e3o foi poss\u00edvel se conectar", + "discover_timeout": "Nenhum Hue bridge descoberto", + "no_bridges": "Nenhum Philips Hue descoberto", + "unknown": "Ocorreu um erro desconhecido" + }, + "error": { + "linking": "Ocorreu um erro de liga\u00e7\u00e3o desconhecido.", + "register_failed": "Falha ao registrar, por favor, tente novamente" + }, + "step": { + "init": { + "data": { + "host": "Servidor" + }, + "title": "Hue bridge" + }, + "link": { + "description": "Pressione o bot\u00e3o no Philips Hue para registrar com o Home Assistant. \n\n ! [Localiza\u00e7\u00e3o do bot\u00e3o] (/ static / images / config_philips_hue.jpg)", + "title": "Link Hub" + } + }, "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/sv.json b/homeassistant/components/hue/.translations/sv.json new file mode 100644 index 00000000000..efbcfa544f5 --- /dev/null +++ b/homeassistant/components/hue/.translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Alla Philips Hue-bryggor \u00e4r redan konfigurerade", + "already_configured": "Bryggan \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta till bryggan", + "discover_timeout": "Det gick inte att uppt\u00e4cka n\u00e5gra Hue-bryggor", + "no_bridges": "Inga Philips Hue-bryggor uppt\u00e4cktes", + "unknown": "Ett ok\u00e4nt fel intr\u00e4ffade" + }, + "error": { + "linking": "Ett ok\u00e4nt l\u00e4nkningsfel intr\u00e4ffade.", + "register_failed": "Misslyckades med att registrera, v\u00e4nligen f\u00f6rs\u00f6k igen" + }, + "step": { + "init": { + "data": { + "host": "V\u00e4rd" + }, + "title": "V\u00e4lj Hue-brygga" + }, + "link": { + "description": "Tryck p\u00e5 knappen p\u00e5 bryggan f\u00f6r att registrera Philips Hue med Home Assistant. \n\n ! [Placering av knapp p\u00e5 brygga] (/ static / images / config_philips_hue.jpg)", + "title": "L\u00e4nka hub" + } + }, + "title": "Philips Hue Brygga" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/vi.json b/homeassistant/components/hue/.translations/vi.json new file mode 100644 index 00000000000..5cbd0c4aebf --- /dev/null +++ b/homeassistant/components/hue/.translations/vi.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "all_configured": "T\u1ea5t c\u1ea3 c\u00e1c c\u1ea7u Philips Hue \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1ea5u h\u00ecnh", + "unknown": "X\u1ea3y ra l\u1ed7i kh\u00f4ng x\u00e1c \u0111\u1ecbnh \u0111\u01b0\u1ee3c" + }, + "error": { + "linking": "\u0110\u00e3 x\u1ea3y ra l\u1ed7i li\u00ean k\u1ebft kh\u00f4ng x\u00e1c \u0111\u1ecbnh.", + "register_failed": "Kh\u00f4ng th\u1ec3 \u0111\u0103ng k\u00fd, vui l\u00f2ng th\u1eed l\u1ea1i" + }, + "step": { + "link": { + "title": "Li\u00ean k\u1ebft Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/ca.json b/homeassistant/components/nest/.translations/ca.json new file mode 100644 index 00000000000..2fb17916aee --- /dev/null +++ b/homeassistant/components/nest/.translations/ca.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s podeu configurar un \u00fanic compte Nest.", + "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", + "authorize_url_timeout": "Temps d'espera generant l'URL d'autoritzaci\u00f3 esgotat.", + "no_flows": "Necessiteu configurar Nest abans de poder autenticar-vos-hi. [Llegiu les instruccions](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Error intern al validar el codi", + "invalid_code": "Codi inv\u00e0lid", + "timeout": "Temps d'espera de validaci\u00f3 del codi esgotat", + "unknown": "Error desconegut al validar el codi" + }, + "step": { + "init": { + "data": { + "flow_impl": "Prove\u00efdor" + }, + "description": "Trieu a trav\u00e9s de quin prove\u00efdor d'autenticaci\u00f3 us voleu autenticar amb Nest.", + "title": "Prove\u00efdor d'autenticaci\u00f3" + }, + "link": { + "data": { + "code": "Codi pin" + }, + "description": "Per enlla\u00e7ar el vostre compte de Nest, [autoritzeu el vostre compte] ({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copieu i enganxeu el codi pin que es mostra a sota.", + "title": "Enlla\u00e7ar compte de Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/ko.json b/homeassistant/components/nest/.translations/ko.json new file mode 100644 index 00000000000..0caa70aeff2 --- /dev/null +++ b/homeassistant/components/nest/.translations/ko.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 Nest \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "no_flows": "Nest \ub97c \uc778\uc99d\ud558\uae30 \uc804\uc5d0 Nest \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/nest/)\ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." + }, + "error": { + "internal_error": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \ub0b4\ubd80 \uc624\ub958 \ubc1c\uc0dd", + "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc", + "timeout": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac \uc2dc\uac04 \ucd08\uacfc", + "unknown": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958 \ubc1c\uc0dd" + }, + "step": { + "init": { + "data": { + "flow_impl": "\uacf5\uae09\uc790" + }, + "description": "Nest\ub85c \uc778\uc99d\ud558\ub824\ub294 \uc778\uc99d \uacf5\uae09\uc790\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "\uc778\uc99d \uacf5\uae09\uc790" + }, + "link": { + "data": { + "code": "\ud540 \ucf54\ub4dc" + }, + "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url})\uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 \ud540 \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.", + "title": "Nest \uacc4\uc815 \uc5f0\uacb0" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/no.json b/homeassistant/components/nest/.translations/no.json new file mode 100644 index 00000000000..03cf1a82b81 --- /dev/null +++ b/homeassistant/components/nest/.translations/no.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bare konfigurere en enkelt Nest konto.", + "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", + "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "no_flows": "Du m\u00e5 konfigurere Nest f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Intern feil ved validering av kode", + "invalid_code": "Ugyldig kode", + "timeout": "Tidsavbrudd ved validering av kode", + "unknown": "Ukjent feil ved validering av kode" + }, + "step": { + "init": { + "data": { + "flow_impl": "Tilbyder" + }, + "description": "Velg via hvilken autentiseringstilbyder du vil godkjenne med Nest.", + "title": "Autentiseringstilbyder" + }, + "link": { + "data": { + "code": "PIN kode" + }, + "description": "For \u00e5 koble din Nest-konto, [autoriser kontoen din]({url}). \n\n Etter godkjenning, kopier og lim inn den oppgitte PIN koden nedenfor.", + "title": "Koble til Nest konto" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/pl.json b/homeassistant/components/nest/.translations/pl.json new file mode 100644 index 00000000000..c03b2eff0fa --- /dev/null +++ b/homeassistant/components/nest/.translations/pl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Nest.", + "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", + "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "no_flows": "Musisz skonfigurowa\u0107 Nest, zanim b\u0119dziesz m\u00f3g\u0142 wykona\u0107 uwierzytelnienie. [Przeczytaj instrukcje](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Wewn\u0119trzny b\u0142\u0105d sprawdzania poprawno\u015bci kodu", + "invalid_code": "Nieprawid\u0142owy kod", + "timeout": "Min\u0105\u0142 limit czasu sprawdzania poprawno\u015bci kodu", + "unknown": "Nieznany b\u0142\u0105d sprawdzania poprawno\u015bci kodu" + }, + "step": { + "init": { + "data": { + "flow_impl": "Dostawca" + }, + "description": "Wybierz, kt\u00f3rego dostawc\u0119 uwierzytelnienia chcesz u\u017cywa\u0107 z Nest.", + "title": "Dostawca uwierzytelnienia" + }, + "link": { + "data": { + "code": "Kod PIN" + }, + "description": "Aby po\u0142\u0105czy\u0107 z kontem Nest, [wykonaj autoryzacj\u0119]({url}). \n\n Po autoryzacji skopiuj i wklej podany kod PIN poni\u017cej.", + "title": "Po\u0142\u0105cz z kontem Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json new file mode 100644 index 00000000000..0f7b9b8dd71 --- /dev/null +++ b/homeassistant/components/nest/.translations/ru.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest.", + "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Nest \u043f\u0435\u0440\u0435\u0434 \u0442\u0435\u043c, \u043a\u0430\u043a \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430", + "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434", + "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430" + }, + "step": { + "init": { + "data": { + "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u0447\u0435\u0440\u0435\u0437 \u043a\u0430\u043a\u043e\u0433\u043e \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0432\u0445\u043e\u0434 \u0432 Nest.", + "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "link": { + "data": { + "code": "\u041f\u0438\u043d-\u043a\u043e\u0434" + }, + "description": " [\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest. \n\n \u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u043f\u0438\u043d-\u043a\u043e\u0434.", + "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/sv.json b/homeassistant/components/nest/.translations/sv.json new file mode 100644 index 00000000000..721f891219d --- /dev/null +++ b/homeassistant/components/nest/.translations/sv.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan endast konfigurera ett Nest-konto.", + "authorize_url_fail": "Ok\u00e4nt fel vid generering av autentisieringsadress.", + "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress.", + "no_flows": "Du m\u00e5ste konfigurera Nest innan du kan autentisera med det. [V\u00e4nligen l\u00e4s instruktionerna] (https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Internt fel vid validering av kod", + "invalid_code": "Ogiltig kod", + "timeout": "Timeout vid valididering av kod", + "unknown": "Ok\u00e4nt fel vid validering av kod" + }, + "step": { + "init": { + "data": { + "flow_impl": "Leverant\u00f6r" + }, + "description": "V\u00e4lj den autentiseringsleverant\u00f6r som du vill autentisera med mot Nest.", + "title": "Autentiseringsleverant\u00f6r" + }, + "link": { + "data": { + "code": "Pin-kod" + }, + "description": "F\u00f6r att l\u00e4nka ditt Nest-konto, [autentisiera ditt konto]({url}). \n\nEfter autentisiering, klipp och klistra in den angivna pin-koden nedan.", + "title": "L\u00e4nka Nest-konto" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/vi.json b/homeassistant/components/nest/.translations/vi.json new file mode 100644 index 00000000000..996c6c68eae --- /dev/null +++ b/homeassistant/components/nest/.translations/vi.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "internal_error": "M\u00e3 x\u00e1c th\u1ef1c l\u1ed7i n\u1ed9i b\u1ed9", + "invalid_code": "M\u00e3 kh\u00f4ng h\u1ee3p l\u1ec7", + "timeout": "M\u00e3 x\u00e1c th\u1ef1c h\u1ebft th\u1eddi gian ch\u1edd", + "unknown": "M\u00e3 x\u00e1c th\u1ef1c l\u1ed7i kh\u00f4ng x\u00e1c \u0111\u1ecbnh" + }, + "step": { + "init": { + "data": { + "flow_impl": "Nh\u00e0 cung c\u1ea5p" + }, + "title": "Nh\u00e0 cung c\u1ea5p x\u00e1c th\u1ef1c" + }, + "link": { + "title": "Li\u00ean k\u1ebft t\u00e0i kho\u1ea3n Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/zh-Hans.json b/homeassistant/components/nest/.translations/zh-Hans.json new file mode 100644 index 00000000000..05ba5bdf155 --- /dev/null +++ b/homeassistant/components/nest/.translations/zh-Hans.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "\u60a8\u53ea\u80fd\u914d\u7f6e\u4e00\u4e2a Nest \u5e10\u6237\u3002", + "authorize_url_fail": "\u751f\u6210\u6388\u6743\u7f51\u5740\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002", + "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002", + "no_flows": "\u60a8\u9700\u8981\u5148\u914d\u7f6e Nest\uff0c\u7136\u540e\u624d\u80fd\u5bf9\u5176\u8fdb\u884c\u6388\u6743\u3002 [\u8bf7\u9605\u8bfb\u8bf4\u660e](https://www.home-assistant.io/components/nest/)\u3002" + }, + "error": { + "internal_error": "\u9a8c\u8bc1\u4ee3\u7801\u65f6\u53d1\u751f\u5185\u90e8\u9519\u8bef", + "invalid_code": "\u65e0\u6548\u4ee3\u7801", + "timeout": "\u4ee3\u7801\u9a8c\u8bc1\u8d85\u65f6", + "unknown": "\u9a8c\u8bc1\u4ee3\u7801\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef" + }, + "step": { + "init": { + "data": { + "flow_impl": "\u63d0\u4f9b\u8005" + }, + "description": "\u9009\u62e9\u60a8\u60f3\u901a\u8fc7\u54ea\u4e2a\u6388\u6743\u63d0\u4f9b\u8005\u4e0e Nest \u8fdb\u884c\u6388\u6743\u3002", + "title": "\u6388\u6743\u63d0\u4f9b\u8005" + }, + "link": { + "data": { + "code": "PIN \u7801" + }, + "description": "\u8981\u5173\u8054 Nest \u5e10\u6237\uff0c\u8bf7[\u6388\u6743\u5e10\u6237]({url})\u3002\n\n\u5b8c\u6210\u6388\u6743\u540e\uff0c\u5728\u4e0b\u9762\u7c98\u8d34\u83b7\u5f97\u7684 PIN \u7801\u3002", + "title": "\u5173\u8054 Nest \u5e10\u6237" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ca.json b/homeassistant/components/sensor/.translations/season.ca.json new file mode 100644 index 00000000000..9bce187ec65 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ca.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Tardor", + "spring": "Primavera", + "summer": "Estiu", + "winter": "Hivern" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.fr.json b/homeassistant/components/sensor/.translations/season.fr.json new file mode 100644 index 00000000000..ec9f9657428 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.fr.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Automne", + "spring": "Printemps", + "summer": "\u00c9t\u00e9", + "winter": "Hiver" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.pt-BR.json b/homeassistant/components/sensor/.translations/season.pt-BR.json new file mode 100644 index 00000000000..fde45ad6c8e --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.pt-BR.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Outono", + "spring": "Primavera", + "summer": "Ver\u00e3o", + "winter": "Inverno" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/ca.json b/homeassistant/components/sonos/.translations/ca.json new file mode 100644 index 00000000000..9a745784b25 --- /dev/null +++ b/homeassistant/components/sonos/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius Sonos a la xarxa.", + "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de Sonos." + }, + "step": { + "confirm": { + "description": "Voleu configurar Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/ko.json b/homeassistant/components/sonos/.translations/ko.json new file mode 100644 index 00000000000..5453e4322cd --- /dev/null +++ b/homeassistant/components/sonos/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Sonos \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "Sonos\uc758 \ub2e8\uc77c \uad6c\uc131 \ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "Sonos\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/no.json b/homeassistant/components/sonos/.translations/no.json new file mode 100644 index 00000000000..c837abad499 --- /dev/null +++ b/homeassistant/components/sonos/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen Sonos enheter funnet p\u00e5 nettverket.", + "single_instance_allowed": "Kun en enkelt konfigurasjon av Sonos er n\u00f8dvendig." + }, + "step": { + "confirm": { + "description": "\u00d8nsker du \u00e5 sette opp Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/pl.json b/homeassistant/components/sonos/.translations/pl.json new file mode 100644 index 00000000000..2a0c526b9a6 --- /dev/null +++ b/homeassistant/components/sonos/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 Sonos.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja Sonos." + }, + "step": { + "confirm": { + "description": "Chcesz skonfigurowa\u0107 Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/ru.json b/homeassistant/components/sonos/.translations/ru.json new file mode 100644 index 00000000000..63b6bd87c20 --- /dev/null +++ b/homeassistant/components/sonos/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Sonos \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Sonos." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/sv.json b/homeassistant/components/sonos/.translations/sv.json new file mode 100644 index 00000000000..756fe8a7483 --- /dev/null +++ b/homeassistant/components/sonos/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga Sonos-enheter hittades i n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration av Sonos \u00e4r n\u00f6dv\u00e4ndig." + }, + "step": { + "confirm": { + "description": "Vill du konfigurera Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/vi.json b/homeassistant/components/sonos/.translations/vi.json new file mode 100644 index 00000000000..ebeb1a8b07c --- /dev/null +++ b/homeassistant/components/sonos/.translations/vi.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Kh\u00f4ng t\u00ecm th\u1ea5y thi\u1ebft b\u1ecb Sonos n\u00e0o tr\u00ean m\u1ea1ng.", + "single_instance_allowed": "Ch\u1ec9 c\u1ea7n m\u1ed9t c\u1ea5u h\u00ecnh duy nh\u1ea5t c\u1ee7a Sonos l\u00e0 \u0111\u1ee7." + }, + "step": { + "confirm": { + "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp Sonos kh\u00f4ng?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/zh-Hans.json b/homeassistant/components/sonos/.translations/zh-Hans.json new file mode 100644 index 00000000000..17c1e78d3e8 --- /dev/null +++ b/homeassistant/components/sonos/.translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 Sonos \u8bbe\u5907\u3002", + "single_instance_allowed": "\u53ea\u6709\u4e00\u6b21 Sonos \u914d\u7f6e\u662f\u5fc5\u8981\u7684\u3002" + }, + "step": { + "confirm": { + "description": "\u60a8\u60f3\u8981\u914d\u7f6e Sonos \u5417\uff1f", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/bg.json b/homeassistant/components/zone/.translations/bg.json new file mode 100644 index 00000000000..5770058c5eb --- /dev/null +++ b/homeassistant/components/zone/.translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" + }, + "step": { + "init": { + "data": { + "icon": "\u0418\u043a\u043e\u043d\u0430", + "latitude": "\u0428\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0414\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435", + "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u0430", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u0437\u043e\u043d\u0430\u0442\u0430" + } + }, + "title": "\u0417\u043e\u043d\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ca.json b/homeassistant/components/zone/.translations/ca.json new file mode 100644 index 00000000000..1676c8f3906 --- /dev/null +++ b/homeassistant/components/zone/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "El nom ja existeix" + }, + "step": { + "init": { + "data": { + "icon": "Icona", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom", + "passive": "Passiu", + "radius": "Radi" + }, + "title": "Defineix els par\u00e0metres de la zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/cs.json b/homeassistant/components/zone/.translations/cs.json new file mode 100644 index 00000000000..a521377e5e0 --- /dev/null +++ b/homeassistant/components/zone/.translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "N\u00e1zev ji\u017e existuje" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "N\u00e1zev", + "passive": "Pasivn\u00ed", + "radius": "Polom\u011br" + }, + "title": "Definujte parametry z\u00f3ny" + } + }, + "title": "Z\u00f3na" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/fr.json b/homeassistant/components/zone/.translations/fr.json new file mode 100644 index 00000000000..eb02aba7b50 --- /dev/null +++ b/homeassistant/components/zone/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9" + }, + "step": { + "init": { + "data": { + "icon": "Ic\u00f4ne", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom", + "passive": "Passif", + "radius": "Rayon" + }, + "title": "D\u00e9finir les param\u00e8tres de la zone" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/hu.json b/homeassistant/components/zone/.translations/hu.json new file mode 100644 index 00000000000..0181f688c27 --- /dev/null +++ b/homeassistant/components/zone/.translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v", + "passive": "Passz\u00edv", + "radius": "Sug\u00e1r" + }, + "title": "Z\u00f3na param\u00e9terek megad\u00e1sa" + } + }, + "title": "Z\u00f3na" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/it.json b/homeassistant/components/zone/.translations/it.json new file mode 100644 index 00000000000..4490124510f --- /dev/null +++ b/homeassistant/components/zone/.translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Il nome \u00e8 gi\u00e0 esistente" + }, + "step": { + "init": { + "data": { + "icon": "Icona", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome", + "passive": "Passiva", + "radius": "Raggio" + }, + "title": "Imposta i parametri della zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ko.json b/homeassistant/components/zone/.translations/ko.json index 364f8f3cc77..421f079a67e 100644 --- a/homeassistant/components/zone/.translations/ko.json +++ b/homeassistant/components/zone/.translations/ko.json @@ -13,7 +13,7 @@ "passive": "\uc790\ub3d9\ud654 \uc804\uc6a9", "radius": "\ubc18\uacbd" }, - "title": "\uad6c\uc5ed \ub9e4\uac1c \ubcc0\uc218 \uc815\uc758" + "title": "\uad6c\uc5ed \uc124\uc815" } }, "title": "\uad6c\uc5ed" diff --git a/homeassistant/components/zone/.translations/pt-BR.json b/homeassistant/components/zone/.translations/pt-BR.json new file mode 100644 index 00000000000..f2a41b0b267 --- /dev/null +++ b/homeassistant/components/zone/.translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "O nome j\u00e1 existe" + }, + "step": { + "init": { + "data": { + "icon": "\u00cdcone", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome", + "passive": "Passivo", + "radius": "Raio" + }, + "title": "Definir par\u00e2metros da zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pt.json b/homeassistant/components/zone/.translations/pt.json index a4ced557805..2c3292e58c1 100644 --- a/homeassistant/components/zone/.translations/pt.json +++ b/homeassistant/components/zone/.translations/pt.json @@ -12,7 +12,8 @@ "name": "Nome", "passive": "Passivo", "radius": "Raio" - } + }, + "title": "Definir os par\u00e2metros da zona" } }, "title": "Zona" diff --git a/homeassistant/components/zone/.translations/sl.json b/homeassistant/components/zone/.translations/sl.json new file mode 100644 index 00000000000..1885cb5d2c8 --- /dev/null +++ b/homeassistant/components/zone/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Ime \u017ee obstaja" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime", + "passive": "Pasivno", + "radius": "Radij" + }, + "title": "Dolo\u010dite parametre obmo\u010dja" + } + }, + "title": "Obmo\u010dje" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/sv.json b/homeassistant/components/zone/.translations/sv.json new file mode 100644 index 00000000000..55c5bcf7127 --- /dev/null +++ b/homeassistant/components/zone/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Namnet finns redan" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn", + "passive": "Passiv", + "radius": "Radie" + }, + "title": "Definiera zonparametrar" + } + }, + "title": "Zon" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/vi.json b/homeassistant/components/zone/.translations/vi.json new file mode 100644 index 00000000000..7217944bd6b --- /dev/null +++ b/homeassistant/components/zone/.translations/vi.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "T\u00ean \u0111\u00e3 t\u1ed3n t\u1ea1i" + }, + "step": { + "init": { + "data": { + "icon": "Bi\u1ec3u t\u01b0\u1ee3ng", + "latitude": "V\u0129 \u0111\u1ed9", + "longitude": "Kinh \u0111\u1ed9", + "name": "T\u00ean", + "passive": "Th\u1ee5 \u0111\u1ed9ng", + "radius": "B\u00e1n k\u00ednh" + }, + "title": "X\u00e1c \u0111\u1ecbnh tham s\u1ed1 v\u00f9ng" + } + }, + "title": "V\u00f9ng" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/zh-Hant.json b/homeassistant/components/zone/.translations/zh-Hant.json new file mode 100644 index 00000000000..12c1141397d --- /dev/null +++ b/homeassistant/components/zone/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728" + }, + "step": { + "init": { + "data": { + "icon": "\u5716\u793a", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31", + "passive": "\u88ab\u52d5", + "radius": "\u534a\u5f91" + }, + "title": "\u5b9a\u7fa9\u5340\u57df\u53c3\u6578" + } + }, + "title": "\u5340\u57df" + } +} \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 4fbbbb77b79..db2912d7b42 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -151,6 +151,8 @@ DISCOVERY_SOURCES = ( data_entry_flow.SOURCE_IMPORT, ) +EVENT_FLOW_DISCOVERED = 'config_entry_discovered' + class ConfigEntry: """Hold a configuration entry.""" @@ -404,6 +406,7 @@ class ConfigEntries: # Create notification. if source in DISCOVERY_SOURCES: + self.hass.bus.async_fire(EVENT_FLOW_DISCOVERED) self.hass.components.persistent_notification.async_create( title='New devices discovered', message=("We have discovered new devices on your network. " From cbdfc95cc8330fec6104a0c30ed7474da7a520f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Jun 2018 23:55:35 -0400 Subject: [PATCH 022/128] Make zone entries work without radius (#15032) --- homeassistant/components/zone/__init__.py | 4 ++-- tests/components/zone/test_init.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index c33a16c632e..ee19e00266c 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -73,8 +73,8 @@ async def async_setup_entry(hass, config_entry): entry = config_entry.data name = entry[CONF_NAME] zone = Zone(hass, name, entry[CONF_LATITUDE], entry[CONF_LONGITUDE], - entry.get(CONF_RADIUS), entry.get(CONF_ICON), - entry.get(CONF_PASSIVE)) + entry.get(CONF_RADIUS, DEFAULT_RADIUS), entry.get(CONF_ICON), + entry.get(CONF_PASSIVE, DEFAULT_PASSIVE)) zone.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, name, None, hass) hass.async_add_job(zone.async_update_ha_state()) diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index c26b3375f3a..92dee05818d 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -17,7 +17,6 @@ async def test_setup_entry_successful(hass): zone.CONF_NAME: 'Test Zone', zone.CONF_LATITUDE: 1.1, zone.CONF_LONGITUDE: -2.2, - zone.CONF_RADIUS: 250, zone.CONF_RADIUS: True } hass.data[zone.DOMAIN] = {} From 0e7e58f1721c8b648edbcde50f0430d041462d17 Mon Sep 17 00:00:00 2001 From: Kees Schollaart Date: Mon, 18 Jun 2018 05:57:08 +0200 Subject: [PATCH 023/128] Update PostNL unit of measure to align with UPS (#15023) I'm using both the UPS and PostNL package trackers. I'd like to have the unit of measure to be the same, now they appear in two different graphs in the history view. If we prefer ```package(s)``` over ```package``` then I'll do a PR for [this line](https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/sensor/ups.py#L81) --- homeassistant/components/sensor/postnl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/postnl.py b/homeassistant/components/sensor/postnl.py index 0e296fa56bd..9b35c1fdc7e 100644 --- a/homeassistant/components/sensor/postnl.py +++ b/homeassistant/components/sensor/postnl.py @@ -76,7 +76,7 @@ class PostNLSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - return 'package(s)' + return 'packages' @property def device_state_attributes(self): From d07e40c483be32b2d3bdf90a2dbdd7d6ebfbabbb Mon Sep 17 00:00:00 2001 From: gstorer Date: Mon, 18 Jun 2018 12:05:14 +0800 Subject: [PATCH 024/128] Expose Wemo component availability to home assistant (#14995) * Expose Wemo component availability to home assistant * Do not add availability feature to dimmer - it works differently * Brain fade, deleted completely the wrong thing. Revert "Do not add availability feature to dimmer - it works differently" This reverts commit f64e7179818fa12d8290ac32de0680025a7eff21. * (2nd attempt) Do not add availability feature to dimmer - it works differently --- homeassistant/components/light/wemo.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index fcf3d2f7a7d..4cd34b698da 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -107,6 +107,11 @@ class WemoLight(Light): """Flag supported features.""" return SUPPORT_WEMO + @property + def available(self): + """Return if light is available.""" + return self.device.state['available'] + def turn_on(self, **kwargs): """Turn the light on.""" transitiontime = int(kwargs.get(ATTR_TRANSITION, 0)) From 7bfa81c592eda8f58b3cce2ee385d35f0ac6c14d Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 17 Jun 2018 21:06:53 -0700 Subject: [PATCH 025/128] Improve volume support for Vizio Smartcast (#14981) * Improve volume support for Vizio Smartcast * Vizio: avoid an error when 'self._device.get_current_volume()' returns 'None' * Improve volume support for Vizio Smartcast * Vizio: avoid an error when 'self._device.get_current_volume()' returns 'None' * First line should end with a period --- .../components/media_player/vizio.py | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/vizio.py b/homeassistant/components/media_player/vizio.py index 381482a4839..81e4c3541d3 100644 --- a/homeassistant/components/media_player/vizio.py +++ b/homeassistant/components/media_player/vizio.py @@ -12,7 +12,8 @@ import voluptuous as vol 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, MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + MediaPlayerDevice) from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) @@ -39,7 +40,8 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) SUPPORTED_COMMANDS = SUPPORT_TURN_ON | SUPPORT_TURN_OFF \ | SUPPORT_SELECT_SOURCE \ | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK \ - | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP \ + | SUPPORT_VOLUME_SET PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -98,7 +100,9 @@ class VizioDevice(MediaPlayerDevice): else: self._state = STATE_ON - self._volume_level = self._device.get_current_volume() + volume = self._device.get_current_volume() + if volume is not None: + self._volume_level = float(volume) / 100. input_ = self._device.get_current_input() if input_ is not None: self._current_input = input_.meta_name @@ -167,12 +171,26 @@ class VizioDevice(MediaPlayerDevice): def volume_up(self): """Increasing volume of the TV.""" + self._volume_level += self._volume_step / 100. self._device.vol_up(num=self._volume_step) def volume_down(self): """Decreasing volume of the TV.""" + self._volume_level -= self._volume_step / 100. self._device.vol_down(num=self._volume_step) def validate_setup(self): """Validate if host is available and key is correct.""" return self._device.get_current_volume() is not None + + def set_volume_level(self, volume): + """Set volume level.""" + if self._volume_level is not None: + if volume > self._volume_level: + num = int(100*(volume - self._volume_level)) + self._volume_level = volume + self._device.vol_up(num=num) + elif volume < self._volume_level: + num = int(100*(self._volume_level - volume)) + self._volume_level = volume + self._device.vol_down(num=num) From f9a21dbfdab10d7652e66eef12937e3ea37677f4 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 18 Jun 2018 15:21:41 +0200 Subject: [PATCH 026/128] Fix linode I/O in state property (#15010) * Fix linode I/O in state property * Move update of all attrs to update --- .../components/binary_sensor/linode.py | 33 ++++++++--------- homeassistant/components/switch/linode.py | 35 ++++++++++--------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/binary_sensor/linode.py b/homeassistant/components/binary_sensor/linode.py index 8af0318373d..d4fc60696cd 100644 --- a/homeassistant/components/binary_sensor/linode.py +++ b/homeassistant/components/binary_sensor/linode.py @@ -52,19 +52,18 @@ class LinodeBinarySensor(BinarySensorDevice): self._node_id = node_id self._state = None self.data = None + self._attrs = {} + self._name = None @property def name(self): """Return the name of the sensor.""" - if self.data is not None: - return self.data.label + return self._name @property def is_on(self): """Return true if the binary sensor is on.""" - if self.data is not None: - return self.data.status == 'running' - return False + return self._state @property def device_class(self): @@ -74,8 +73,18 @@ class LinodeBinarySensor(BinarySensorDevice): @property def device_state_attributes(self): """Return the state attributes of the Linode Node.""" - if self.data: - return { + return self._attrs + + def update(self): + """Update state of sensor.""" + self._linode.update() + if self._linode.data is not None: + for node in self._linode.data: + if node.id == self._node_id: + self.data = node + if self.data is not None: + self._state = self.data.status == 'running' + self._attrs = { ATTR_CREATED: self.data.created, ATTR_NODE_ID: self.data.id, ATTR_NODE_NAME: self.data.label, @@ -85,12 +94,4 @@ class LinodeBinarySensor(BinarySensorDevice): ATTR_REGION: self.data.region.country, ATTR_VCPUS: self.data.specs.vcpus, } - return {} - - def update(self): - """Update state of sensor.""" - self._linode.update() - if self._linode.data is not None: - for node in self._linode.data: - if node.id == self._node_id: - self.data = node + self._name = self.data.label diff --git a/homeassistant/components/switch/linode.py b/homeassistant/components/switch/linode.py index 91177e32116..43f4bdc31b4 100644 --- a/homeassistant/components/switch/linode.py +++ b/homeassistant/components/switch/linode.py @@ -51,35 +51,23 @@ class LinodeSwitch(SwitchDevice): self._node_id = node_id self.data = None self._state = None + self._attrs = {} + self._name = None @property def name(self): """Return the name of the switch.""" - if self.data is not None: - return self.data.label + return self._name @property def is_on(self): """Return true if switch is on.""" - if self.data is not None: - return self.data.status == 'running' - return False + return self._state @property def device_state_attributes(self): """Return the state attributes of the Linode Node.""" - if self.data: - return { - ATTR_CREATED: self.data.created, - ATTR_NODE_ID: self.data.id, - ATTR_NODE_NAME: self.data.label, - ATTR_IPV4_ADDRESS: self.data.ipv4, - ATTR_IPV6_ADDRESS: self.data.ipv6, - ATTR_MEMORY: self.data.specs.memory, - ATTR_REGION: self.data.region.country, - ATTR_VCPUS: self.data.specs.vcpus, - } - return {} + return self._attrs def turn_on(self, **kwargs): """Boot-up the Node.""" @@ -98,3 +86,16 @@ class LinodeSwitch(SwitchDevice): for node in self._linode.data: if node.id == self._node_id: self.data = node + if self.data is not None: + self._state = self.data.status == 'running' + self._attrs = { + ATTR_CREATED: self.data.created, + ATTR_NODE_ID: self.data.id, + ATTR_NODE_NAME: self.data.label, + ATTR_IPV4_ADDRESS: self.data.ipv4, + ATTR_IPV6_ADDRESS: self.data.ipv6, + ATTR_MEMORY: self.data.specs.memory, + ATTR_REGION: self.data.region.country, + ATTR_VCPUS: self.data.specs.vcpus, + } + self._name = self.data.label From 62f970e48686c88b021d559153d9b4dc0004c2d3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 18 Jun 2018 15:22:52 +0200 Subject: [PATCH 027/128] Bugfix empty entity lists (#15035) * Bugfix empty entity lists * Add tests * Update test_entity_platform.py * Update entity_platform.py --- homeassistant/helpers/entity_platform.py | 4 ++++ tests/helpers/test_entity_platform.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ab6c3a084c0..472a88888d8 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -216,6 +216,10 @@ class EntityPlatform(object): component_entities, registry) for entity in new_entities] + # No entities for processing + if not tasks: + return + await asyncio.wait(tasks, loop=self.hass.loop) self.async_entities_added_callback() diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 9fa178022dc..2d2f148189f 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -592,3 +592,13 @@ async def test_reset_cancels_retry_setup(hass): assert len(mock_call_later.return_value.mock_calls) == 1 assert ent_platform._async_cancel_retry_setup is None + + +@asyncio.coroutine +def test_not_fails_with_adding_empty_entities_(hass): + """Test for not fails on empty entities list.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + yield from component.async_add_entities([]) + + assert len(hass.states.async_entity_ids()) == 0 From 8869617890b7822d9a916d1720b926df29799444 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 18 Jun 2018 09:58:16 -0400 Subject: [PATCH 028/128] Bump frontend to 20180618.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 25aa0da0a3e..2c9b68bf079 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180617.0'] +REQUIREMENTS = ['home-assistant-frontend==20180618.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 0afb9d01f44..83763b569b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -412,7 +412,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180617.0 +home-assistant-frontend==20180618.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b066a52b3c6..6990bb23849 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==20180617.0 +home-assistant-frontend==20180618.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 9d6ce609f9f65b5c6992f2f099dde7d3dec94b3b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 18 Jun 2018 18:13:21 +0200 Subject: [PATCH 029/128] Upgrade requests to 2.19.1 (#15019) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 85f39cfb1b7..32374b90135 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,7 +7,7 @@ jinja2>=2.10 pip>=8.0.3 pytz>=2018.04 pyyaml>=3.11,<4 -requests==2.18.4 +requests==2.19.1 voluptuous==0.11.1 # Breaks Python 3.6 and is not needed for our supported Python versions diff --git a/requirements_all.txt b/requirements_all.txt index 83763b569b2..64bc53d5c89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,7 +8,7 @@ jinja2>=2.10 pip>=8.0.3 pytz>=2018.04 pyyaml>=3.11,<4 -requests==2.18.4 +requests==2.19.1 voluptuous==0.11.1 # homeassistant.components.nuimo_controller diff --git a/setup.py b/setup.py index c2ea6c87cc9..69929285f78 100755 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ REQUIRES = [ 'pip>=8.0.3', 'pytz>=2018.04', 'pyyaml>=3.11,<4', - 'requests==2.18.4', + 'requests==2.19.1', 'voluptuous==0.11.1', ] From 067e4f6d9a3bcc2723368ad10493af7a3cdb40f3 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Mon, 18 Jun 2018 09:13:50 -0700 Subject: [PATCH 030/128] Show running apps as sources for Fire TV (#15033) * Show running apps as sources for Fire TV * Fix unnecessary 'else' after 'return' (no-else-return) * Remove 'pylint: disable=unused-argument' * cleanup --- .../components/media_player/firetv.py | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py index 157db2c44d3..280a84f0828 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -11,8 +11,8 @@ import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, PLATFORM_SCHEMA, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_SET, SUPPORT_PLAY, - MediaPlayerDevice) + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_SET, SUPPORT_PLAY, MediaPlayerDevice) from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, STATE_UNKNOWN, CONF_HOST, CONF_PORT, CONF_SSL, CONF_NAME, CONF_DEVICE, @@ -23,7 +23,8 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_FIRETV = SUPPORT_PAUSE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET | SUPPORT_PLAY + SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_VOLUME_SET | \ + SUPPORT_PLAY DEFAULT_SSL = False DEFAULT_DEVICE = 'default' @@ -33,6 +34,7 @@ DEFAULT_PORT = 5556 DEVICE_ACTION_URL = '{0}://{1}:{2}/devices/action/{3}/{4}' DEVICE_LIST_URL = '{0}://{1}:{2}/devices/list' DEVICE_STATE_URL = '{0}://{1}:{2}/devices/state/{3}' +DEVICE_APPS_URL = '{0}://{1}:{2}/devices/{3}/apps/{4}' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, @@ -98,6 +100,38 @@ class FireTV(object): "Could not retrieve device state for %s", self.device_id) return STATE_UNKNOWN + @property + def current_app(self): + """Return the current app.""" + try: + response = requests.get( + DEVICE_APPS_URL.format( + self.proto, self.host, self.port, self.device_id, 'current' + ), timeout=10).json() + _current_app = response.get('current_app') + if _current_app: + return _current_app.get('package') + + return None + except requests.exceptions.RequestException: + _LOGGER.error( + "Could not retrieve current app for %s", self.device_id) + return None + + @property + def running_apps(self): + """Return a list of running apps.""" + try: + response = requests.get( + DEVICE_APPS_URL.format( + self.proto, self.host, self.port, self.device_id, 'running' + ), timeout=10).json() + return response.get('running_apps') + except requests.exceptions.RequestException: + _LOGGER.error( + "Could not retrieve running apps for %s", self.device_id) + return None + def action(self, action_id): """Perform an action on the device.""" try: @@ -109,6 +143,16 @@ class FireTV(object): "Action request for %s was not accepted for device %s", action_id, self.device_id) + def start_app(self, app_name): + """Start an app.""" + try: + requests.get(DEVICE_APPS_URL.format( + self.proto, self.host, self.port, self.device_id, + app_name + '/start'), timeout=10) + except requests.exceptions.RequestException: + _LOGGER.error( + "Could not start %s on %s", app_name, self.device_id) + class FireTVDevice(MediaPlayerDevice): """Representation of an Amazon Fire TV device on the network.""" @@ -118,6 +162,8 @@ class FireTVDevice(MediaPlayerDevice): self._firetv = FireTV(proto, host, port, device) self._name = name self._state = STATE_UNKNOWN + self._running_apps = None + self._current_app = None @property def name(self): @@ -139,6 +185,16 @@ class FireTVDevice(MediaPlayerDevice): """Return the state of the player.""" return self._state + @property + def source(self): + """Return the current app.""" + return self._current_app + + @property + def source_list(self): + """Return a list of running apps.""" + return self._running_apps + def update(self): """Get the latest date and update device state.""" self._state = { @@ -150,6 +206,13 @@ class FireTVDevice(MediaPlayerDevice): 'disconnected': STATE_UNKNOWN, }.get(self._firetv.state, STATE_UNKNOWN) + if self._state not in [STATE_OFF, STATE_UNKNOWN]: + self._running_apps = self._firetv.running_apps + self._current_app = self._firetv.current_app + else: + self._running_apps = None + self._current_app = None + def turn_on(self): """Turn on the device.""" self._firetv.action('turn_on') @@ -185,3 +248,7 @@ class FireTVDevice(MediaPlayerDevice): def media_next_track(self): """Send next track command (results in fast-forward).""" self._firetv.action('media_next') + + def select_source(self, source): + """Select input source.""" + self._firetv.start_app(source) From 153ccda853bd7f5dba5ec79ec713500943e999bf Mon Sep 17 00:00:00 2001 From: Hate-Usernames Date: Tue, 19 Jun 2018 02:34:36 +0100 Subject: [PATCH 031/128] Patch save_json (#15046) --- tests/components/light/test_tradfri.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/light/test_tradfri.py b/tests/components/light/test_tradfri.py index 8ef5d17452a..12c596f3f09 100644 --- a/tests/components/light/test_tradfri.py +++ b/tests/components/light/test_tradfri.py @@ -229,6 +229,7 @@ async def setup_gateway(hass, mock_gateway, mock_api, patch('pytradfri.api.aiocoap_api.APIFactory.request', mock_api), \ patch('pytradfri.Gateway', return_value=mock_gateway), \ patch.object(tradfri, 'load_json', return_value=known_hosts), \ + patch.object(tradfri, 'save_json'), \ patch.object(hass.components.configurator, 'request_config', request_config): From 7a180ac2058fbee8488557b949d6889ac09c2c8c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 19 Jun 2018 09:56:29 +0200 Subject: [PATCH 032/128] Update loopenergy link to docs (#15050) --- homeassistant/components/sensor/loopenergy.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/loopenergy.py b/homeassistant/components/sensor/loopenergy.py index 8bf95d4ef6e..09ed4ab3d49 100644 --- a/homeassistant/components/sensor/loopenergy.py +++ b/homeassistant/components/sensor/loopenergy.py @@ -2,18 +2,18 @@ Support for Loop Energy sensors. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.loop_energy/ +https://home-assistant.io/components/sensor.loopenergy/ """ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.const import ( - CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL) from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, + EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -47,12 +47,12 @@ GAS_SCHEMA = vol.Schema({ vol.Optional(CONF_GAS_TYPE, default=CONF_UNIT_SYSTEM_METRIC): GAS_TYPE_SCHEMA, vol.Optional(CONF_GAS_CALORIFIC, default=DEFAULT_CALORIFIC): - vol.Coerce(float) + vol.Coerce(float), }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ELEC): ELEC_SCHEMA, - vol.Optional(CONF_GAS): GAS_SCHEMA + vol.Optional(CONF_GAS): GAS_SCHEMA, }) From 7cd620d30f442e97d5753e9adecb8ff40aab69e2 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 19 Jun 2018 11:30:43 +0300 Subject: [PATCH 033/128] Switch upstream adafruit package (#15038) --- homeassistant/components/sensor/dht.py | 5 +---- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py index cbf06783dc7..b22e4df9a50 100644 --- a/homeassistant/components/sensor/dht.py +++ b/homeassistant/components/sensor/dht.py @@ -17,10 +17,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit -# Update this requirement to upstream as soon as it supports Python 3. -REQUIREMENTS = ['https://github.com/adafruit/Adafruit_Python_DHT/archive/' - 'da8cddf7fb629c1ef4f046ca44f42523c9cf2d11.zip' - '#Adafruit_DHT==1.3.2'] +REQUIREMENTS = ['Adafruit_Python_DHT==1.3.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 64bc53d5c89..5a0d4c69f69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -23,6 +23,9 @@ Adafruit-SHT31==1.0.2 # homeassistant.components.bbb_gpio # Adafruit_BBIO==1.0.0 +# homeassistant.components.sensor.dht +# Adafruit_Python_DHT==1.3.2 + # homeassistant.components.doorbird DoorBirdPy==0.1.3 @@ -426,9 +429,6 @@ http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b89974819 # homeassistant.components.remember_the_milk httplib2==0.10.3 -# homeassistant.components.sensor.dht -# https://github.com/adafruit/Adafruit_Python_DHT/archive/da8cddf7fb629c1ef4f046ca44f42523c9cf2d11.zip#Adafruit_DHT==1.3.2 - # homeassistant.components.neato https://github.com/jabesq/pybotvac/archive/v0.0.5.zip#pybotvac==0.0.5 From 1e7333eeb6fad3ccfd93cb221590c25d46232109 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 19 Jun 2018 11:31:21 +0300 Subject: [PATCH 034/128] Switch to own packaged version of pyflic (#15041) --- homeassistant/components/binary_sensor/flic.py | 2 +- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/flic.py b/homeassistant/components/binary_sensor/flic.py index 170f1818a0e..baf1d469b28 100644 --- a/homeassistant/components/binary_sensor/flic.py +++ b/homeassistant/components/binary_sensor/flic.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) -REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4'] +REQUIREMENTS = ['pyflic-homeassistant==0.4.dev0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5a0d4c69f69..547681d99f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -435,9 +435,6 @@ https://github.com/jabesq/pybotvac/archive/v0.0.5.zip#pybotvac==0.0.5 # homeassistant.components.sensor.gtfs https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e2246267dd8.zip#pygtfs==0.1.3 -# homeassistant.components.binary_sensor.flic -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 @@ -824,6 +821,9 @@ pyfido==2.1.1 # homeassistant.components.climate.flexit pyflexit==0.3 +# homeassistant.components.binary_sensor.flic +pyflic-homeassistant==0.4.dev0 + # homeassistant.components.fritzbox pyfritzhome==0.3.7 From 27873b4457d63064785c35f4d0dd1e85fad347ef Mon Sep 17 00:00:00 2001 From: Raoul Teeuwen Date: Tue, 19 Jun 2018 13:26:52 +0200 Subject: [PATCH 035/128] Update condition.py (#15021) * Update condition.py Added code that writes to warning-log what entity causes a problem when 'value cannot be processed as a number', making troubleshooting easier. * Make a one-line warning --- homeassistant/helpers/condition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index cb577e8a9c7..921b3bcf06b 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -166,7 +166,8 @@ def async_numeric_state(hass: HomeAssistant, entity, below=None, above=None, try: value = float(value) except ValueError: - _LOGGER.warning("Value cannot be processed as a number: %s", value) + _LOGGER.warning("Value cannot be processed as a number: %s " + "(Offending entity: %s)", entity, value) return False if below is not None and value >= below: From 62432ced9032c9945c90a84caa10169a99307131 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Jun 2018 10:56:33 -0400 Subject: [PATCH 036/128] Update frontend to 20180619.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 2c9b68bf079..9af1a7af3be 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180618.0'] +REQUIREMENTS = ['home-assistant-frontend==20180619.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 547681d99f4..2484712e570 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180618.0 +home-assistant-frontend==20180619.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6990bb23849..b44f2e99f47 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==20180618.0 +home-assistant-frontend==20180619.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From fca5d55b432717901f31348dfd08ce38741c6d72 Mon Sep 17 00:00:00 2001 From: gstorer Date: Tue, 19 Jun 2018 23:16:31 +0800 Subject: [PATCH 037/128] Update pywemo to version 0.4.28. (#15052) --- homeassistant/components/wemo.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index 15b75b2f7a8..d38a42e2cbf 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -14,7 +14,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['pywemo==0.4.25'] +REQUIREMENTS = ['pywemo==0.4.28'] DOMAIN = 'wemo' diff --git a/requirements_all.txt b/requirements_all.txt index 2484712e570..93d34fa1d30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1152,7 +1152,7 @@ pyvlx==0.1.3 pywebpush==1.6.0 # homeassistant.components.wemo -pywemo==0.4.25 +pywemo==0.4.28 # homeassistant.components.camera.xeoma pyxeoma==1.4.0 From 1f8699d9b409cfc5f2ea3ebedc72b3a5c1c2ec99 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Wed, 20 Jun 2018 01:50:38 -0400 Subject: [PATCH 038/128] Upgrade pyarlo to 0.1.8 to support Arlo Baby monitor (#15060) --- homeassistant/components/arlo.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index cd2c13ad292..fa58c9b0baa 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.dispatcher import dispatcher_send -REQUIREMENTS = ['pyarlo==0.1.7'] +REQUIREMENTS = ['pyarlo==0.1.8'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 93d34fa1d30..c851ad8f01d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -739,7 +739,7 @@ pyairvisual==2.0.1 pyalarmdotcom==0.3.2 # homeassistant.components.arlo -pyarlo==0.1.7 +pyarlo==0.1.8 # homeassistant.components.notify.xmpp pyasn1-modules==0.1.5 From 75580dfaded3763d6ee5971e0972829d36a1927e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dejan=20Daki=C4=87?= Date: Wed, 20 Jun 2018 09:15:40 +0200 Subject: [PATCH 039/128] Upgraded librouteros since it has support for authenticatoin in new RouterOS. (#15056) --- homeassistant/components/device_tracker/mikrotik.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py index a6a67749f76..e9a7efeb64a 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) -REQUIREMENTS = ['librouteros==1.0.5'] +REQUIREMENTS = ['librouteros==2.1.0'] MTK_DEFAULT_API_PORT = '8728' diff --git a/requirements_all.txt b/requirements_all.txt index c851ad8f01d..9b887ec388b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -504,7 +504,7 @@ libpurecoollink==0.4.2 libpyfoscam==1.0 # homeassistant.components.device_tracker.mikrotik -librouteros==1.0.5 +librouteros==2.1.0 # homeassistant.components.media_player.soundtouch libsoundtouch==0.7.2 From 6bc03ee763822222aa32bc29abb8f9ecbc979f89 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 20 Jun 2018 09:41:28 -0400 Subject: [PATCH 040/128] Python wink update (#15048) * Updated python-wink to 1.9.0 * Added support for groups of Wink shades. --- homeassistant/components/cover/wink.py | 4 ++++ homeassistant/components/wink/__init__.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/wink.py b/homeassistant/components/cover/wink.py index 093ccd43473..7f7a3a11644 100644 --- a/homeassistant/components/cover/wink.py +++ b/homeassistant/components/cover/wink.py @@ -21,6 +21,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _id = shade.object_id() + shade.name() if _id not in hass.data[DOMAIN]['unique_ids']: add_devices([WinkCoverDevice(shade, hass)]) + for shade in pywink.get_shade_groups(): + _id = shade.object_id() + shade.name() + if _id not in hass.data[DOMAIN]['unique_ids']: + add_devices([WinkCoverDevice(shade, hass)]) for door in pywink.get_garage_doors(): _id = door.object_id() + door.name() if _id not in hass.data[DOMAIN]['unique_ids']: diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index e4dfc17246a..7016250c6b1 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import track_time_interval from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['python-wink==1.8.0', 'pubnubsub-handler==1.0.2'] +REQUIREMENTS = ['python-wink==1.9.0', 'pubnubsub-handler==1.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9b887ec388b..b8587b9df2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1098,7 +1098,7 @@ python-velbus==2.0.11 python-vlc==1.1.2 # homeassistant.components.wink -python-wink==1.8.0 +python-wink==1.9.0 # homeassistant.components.sensor.swiss_public_transport python_opendata_transport==0.1.3 From a72974275775871cf4dd45d4b4600268b6f77dc3 Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Wed, 20 Jun 2018 13:29:36 -0400 Subject: [PATCH 041/128] Fix tplink max/min kelvin for temperature adjustment (#15020) --- homeassistant/components/light/tplink.py | 12 +++++++++++- homeassistant/components/switch/tplink.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index d7544cb6c5a..09a4fa3610d 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -19,7 +19,7 @@ from homeassistant.util.color import \ from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired) -REQUIREMENTS = ['pyHS100==0.3.1'] +REQUIREMENTS = ['pyHS100==0.3.2'] _LOGGER = logging.getLogger(__name__) @@ -104,6 +104,16 @@ class TPLinkSmartBulb(Light): """Turn the light off.""" self.smartbulb.state = self.smartbulb.BULB_STATE_OFF + @property + def min_mireds(self): + """Return minimum supported color temperature.""" + return kelvin_to_mired(self.smartbulb.valid_temperature_range[1]) + + @property + def max_mireds(self): + """Return maximum supported color temperature.""" + return kelvin_to_mired(self.smartbulb.valid_temperature_range[0]) + @property def color_temp(self): """Return the color temperature of this light in mireds for HA.""" diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py index 46682d87356..eb54e7982a7 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/switch/tplink.py @@ -14,7 +14,7 @@ from homeassistant.components.switch import ( from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_VOLTAGE) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyHS100==0.3.1'] +REQUIREMENTS = ['pyHS100==0.3.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index b8587b9df2b..d338edf9916 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -715,7 +715,7 @@ pyCEC==0.4.13 # homeassistant.components.light.tplink # homeassistant.components.switch.tplink -pyHS100==0.3.1 +pyHS100==0.3.2 # homeassistant.components.rfxtrx pyRFXtrx==0.22.1 From 895306f8228f58cab49c83a87db7077978c9dfa0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Jun 2018 15:13:08 -0400 Subject: [PATCH 042/128] Rename experimental UI to lovelace (#15065) * Rename experimental UI to lovelace * Bump frontend to 20180620.0 --- homeassistant/components/frontend/__init__.py | 42 ++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/frontend/__init__.py | 1 + .../test_init.py} | 45 +++++++++++++++++-- 5 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 tests/components/frontend/__init__.py rename tests/components/{test_frontend.py => frontend/test_init.py} (86%) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9af1a7af3be..b2cac55bd77 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,11 +21,12 @@ from homeassistant.components import websocket_api from homeassistant.config import find_config_file, load_yaml_config_file from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180619.0'] +REQUIREMENTS = ['home-assistant-frontend==20180620.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -106,9 +107,9 @@ SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_GET_TRANSLATIONS, vol.Required('language'): str, }) -WS_TYPE_GET_EXPERIMENTAL_UI = 'frontend/experimental_ui' -SCHEMA_GET_EXPERIMENTAL_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_GET_EXPERIMENTAL_UI, +WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' +SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_LOVELACE_UI, }) @@ -216,8 +217,8 @@ async def async_setup(hass, config): WS_TYPE_GET_TRANSLATIONS, websocket_get_translations, SCHEMA_GET_TRANSLATIONS) hass.components.websocket_api.async_register_command( - WS_TYPE_GET_EXPERIMENTAL_UI, websocket_experimental_config, - SCHEMA_GET_EXPERIMENTAL_UI) + WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, + SCHEMA_GET_LOVELACE_UI) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -265,7 +266,7 @@ async def async_setup(hass, config): await asyncio.wait( [async_register_built_in_panel(hass, panel) for panel in ( 'dev-event', 'dev-info', 'dev-service', 'dev-state', - 'dev-template', 'dev-mqtt', 'kiosk', 'experimental-ui')], + 'dev-template', 'dev-mqtt', 'kiosk', 'lovelace')], loop=hass.loop) hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel @@ -499,15 +500,26 @@ def websocket_get_translations(hass, connection, msg): hass.async_add_job(send_translations()) -def websocket_experimental_config(hass, connection, msg): - """Send experimental UI config over websocket config.""" +def websocket_lovelace_config(hass, connection, msg): + """Send lovelace UI config over websocket config.""" async def send_exp_config(): - """Send experimental frontend config.""" - config = await hass.async_add_job( - load_yaml, hass.config.path('experimental-ui.yaml')) + """Send lovelace frontend config.""" + error = None + try: + config = await hass.async_add_job( + load_yaml, hass.config.path('ui-lovelace.yaml')) + message = websocket_api.result_message( + msg['id'], config + ) + except FileNotFoundError: + error = ('file_not_found', + 'Could not find ui-lovelace.yaml in your config dir.') + except HomeAssistantError as err: + error = 'load_error', str(err) - connection.send_message_outside(websocket_api.result_message( - msg['id'], config - )) + if error is not None: + message = websocket_api.error_message(msg['id'], *error) + + connection.send_message_outside(message) hass.async_add_job(send_exp_config()) diff --git a/requirements_all.txt b/requirements_all.txt index d338edf9916..5b83edac4a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180619.0 +home-assistant-frontend==20180620.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b44f2e99f47..7e12ef3910a 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==20180619.0 +home-assistant-frontend==20180620.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb diff --git a/tests/components/frontend/__init__.py b/tests/components/frontend/__init__.py new file mode 100644 index 00000000000..991a74dee7a --- /dev/null +++ b/tests/components/frontend/__init__.py @@ -0,0 +1 @@ +"""Tests for the frontend component.""" diff --git a/tests/components/test_frontend.py b/tests/components/frontend/test_init.py similarity index 86% rename from tests/components/test_frontend.py rename to tests/components/frontend/test_init.py index cb0c72e9edd..2125668facb 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/frontend/test_init.py @@ -5,6 +5,7 @@ from unittest.mock import patch import pytest +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.components.frontend import ( DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL, @@ -280,8 +281,8 @@ async def test_get_translations(hass, hass_ws_client): assert msg['result'] == {'resources': {'lang': 'nl'}} -async def test_experimental_ui(hass, hass_ws_client): - """Test experimental_ui command.""" +async def test_lovelace_ui(hass, hass_ws_client): + """Test lovelace_ui command.""" await async_setup_component(hass, 'frontend') client = await hass_ws_client(hass) @@ -289,7 +290,7 @@ async def test_experimental_ui(hass, hass_ws_client): return_value={'hello': 'world'}): await client.send_json({ 'id': 5, - 'type': 'frontend/experimental_ui', + 'type': 'frontend/lovelace_config', }) msg = await client.receive_json() @@ -297,3 +298,41 @@ async def test_experimental_ui(hass, hass_ws_client): assert msg['type'] == wapi.TYPE_RESULT assert msg['success'] assert msg['result'] == {'hello': 'world'} + + +async def test_lovelace_ui_not_found(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'frontend') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.frontend.load_yaml', + side_effect=FileNotFoundError): + await client.send_json({ + 'id': 5, + 'type': 'frontend/lovelace_config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'file_not_found' + + +async def test_lovelace_ui_load_err(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'frontend') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.frontend.load_yaml', + side_effect=HomeAssistantError): + await client.send_json({ + 'id': 5, + 'type': 'frontend/lovelace_config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'load_error' From be6d1b5e9487e24b16e615eb8294fef04849cf5f Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 20 Jun 2018 20:44:05 -0500 Subject: [PATCH 043/128] X10 (#14741) * Implement X10 * Add X10 after add_device_callback * Ref device by id not hex and add x10OnOffSwitch name * X10 services and add sensor device * Correctly reference X10_HOUSECODE_SCHEMA * Log adding of X10 devices * Add X10 All Units Off, All Lights On and All Lights Off devices * Correct ref to X10 states vs devices * Add X10 All Units Off, All Lights On and All Lights Off devices * Correct X10 config * Debug x10 device additions * Config x10 from bool to housecode char * Pass PLM to X10 device create * Remove PLM to call to add_x10_device * Unconfuse x10 config and method names * Correct spelling of x10_all_lights_off_housecode * Bump insteonplm to 0.10.0 to support X10 --- .../components/insteon_plm/__init__.py | 111 +++++++++++++++++- .../components/insteon_plm/services.yaml | 18 +++ .../components/switch/insteon_plm.py | 3 +- requirements_all.txt | 2 +- 4 files changed, 128 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py index b86f80cbee7..b2f7c8b6655 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.2'] +REQUIREMENTS = ['insteonplm==0.10.0'] _LOGGER = logging.getLogger(__name__) @@ -29,17 +29,31 @@ CONF_CAT = 'cat' CONF_SUBCAT = 'subcat' CONF_FIRMWARE = 'firmware' CONF_PRODUCT_KEY = 'product_key' +CONF_X10 = 'x10_devices' +CONF_HOUSECODE = 'housecode' +CONF_UNITCODE = 'unitcode' +CONF_DIM_STEPS = 'dim_steps' +CONF_X10_ALL_UNITS_OFF = 'x10_all_units_off' +CONF_X10_ALL_LIGHTS_ON = 'x10_all_lights_on' +CONF_X10_ALL_LIGHTS_OFF = 'x10_all_lights_off' SRV_ADD_ALL_LINK = 'add_all_link' SRV_DEL_ALL_LINK = 'delete_all_link' SRV_LOAD_ALDB = 'load_all_link_database' SRV_PRINT_ALDB = 'print_all_link_database' SRV_PRINT_IM_ALDB = 'print_im_all_link_database' +SRV_X10_ALL_UNITS_OFF = 'x10_all_units_off' +SRV_X10_ALL_LIGHTS_OFF = 'x10_all_lights_off' +SRV_X10_ALL_LIGHTS_ON = 'x10_all_lights_on' SRV_ALL_LINK_GROUP = 'group' SRV_ALL_LINK_MODE = 'mode' SRV_LOAD_DB_RELOAD = 'reload' SRV_CONTROLLER = 'controller' SRV_RESPONDER = 'responder' +SRV_HOUSECODE = 'housecode' + +HOUSECODES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p'] CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( cv.deprecated(CONF_PLATFORM), vol.Schema({ @@ -51,11 +65,24 @@ CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( vol.Optional(CONF_PLATFORM): cv.string, })) +CONF_X10_SCHEMA = vol.All( + vol.Schema({ + vol.Required(CONF_HOUSECODE): cv.string, + vol.Required(CONF_UNITCODE): vol.Range(min=1, max=16), + vol.Required(CONF_PLATFORM): cv.string, + vol.Optional(CONF_DIM_STEPS): vol.Range(min=2, max=255) + })) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_PORT): cv.string, vol.Optional(CONF_OVERRIDE): vol.All( - cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]) + cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]), + vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES), + vol.Optional(CONF_X10_ALL_LIGHTS_ON): vol.In(HOUSECODES), + vol.Optional(CONF_X10_ALL_LIGHTS_OFF): vol.In(HOUSECODES), + vol.Optional(CONF_X10): vol.All( + cv.ensure_list_csv, [CONF_X10_SCHEMA]) }) }, extra=vol.ALLOW_EXTRA) @@ -77,6 +104,10 @@ PRINT_ALDB_SCHEMA = vol.Schema({ vol.Required(CONF_ENTITY_ID): cv.entity_id, }) +X10_HOUSECODE_SCHEMA = vol.Schema({ + vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES), + }) + @asyncio.coroutine def async_setup(hass, config): @@ -89,6 +120,10 @@ def async_setup(hass, config): conf = config[DOMAIN] port = conf.get(CONF_PORT) overrides = conf.get(CONF_OVERRIDE, []) + x10_devices = conf.get(CONF_X10, []) + x10_all_units_off_housecode = conf.get(CONF_X10_ALL_UNITS_OFF) + x10_all_lights_on_housecode = conf.get(CONF_X10_ALL_LIGHTS_ON) + x10_all_lights_off_housecode = conf.get(CONF_X10_ALL_LIGHTS_OFF) @callback def async_plm_new_device(device): @@ -106,7 +141,7 @@ def async_setup(hass, config): hass.async_add_job( discovery.async_load_platform( hass, platform, DOMAIN, - discovered={'address': device.address.hex, + discovered={'address': device.address.id, 'state_key': state_key}, hass_config=config)) @@ -151,6 +186,21 @@ def async_setup(hass, config): # Furture direction is to create an INSTEON control panel. print_aldb_to_log(plm.aldb) + def x10_all_units_off(service): + """Send the X10 All Units Off command.""" + housecode = service.data.get(SRV_HOUSECODE) + plm.x10_all_units_off(housecode) + + def x10_all_lights_off(service): + """Send the X10 All Lights Off command.""" + housecode = service.data.get(SRV_HOUSECODE) + plm.x10_all_lights_off(housecode) + + def x10_all_lights_on(service): + """Send the X10 All Lights On command.""" + housecode = service.data.get(SRV_HOUSECODE) + plm.x10_all_lights_on(housecode) + def _register_services(): hass.services.register(DOMAIN, SRV_ADD_ALL_LINK, add_all_link, schema=ADD_ALL_LINK_SCHEMA) @@ -162,6 +212,15 @@ def async_setup(hass, config): schema=PRINT_ALDB_SCHEMA) hass.services.register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None) + hass.services.register(DOMAIN, SRV_X10_ALL_UNITS_OFF, + x10_all_units_off, + schema=X10_HOUSECODE_SCHEMA) + hass.services.register(DOMAIN, SRV_X10_ALL_LIGHTS_OFF, + x10_all_lights_off, + schema=X10_HOUSECODE_SCHEMA) + hass.services.register(DOMAIN, SRV_X10_ALL_LIGHTS_ON, + x10_all_lights_on, + schema=X10_HOUSECODE_SCHEMA) _LOGGER.debug("Insteon_plm Services registered") _LOGGER.info("Looking for PLM on %s", port) @@ -192,6 +251,36 @@ def async_setup(hass, config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close) plm.devices.add_device_callback(async_plm_new_device) + + if x10_all_units_off_housecode: + device = plm.add_x10_device(x10_all_units_off_housecode, + 20, + 'allunitsoff') + if x10_all_lights_on_housecode: + device = plm.add_x10_device(x10_all_lights_on_housecode, + 21, + 'alllightson') + if x10_all_lights_off_housecode: + device = plm.add_x10_device(x10_all_lights_off_housecode, + 22, + 'alllightsoff') + for device in x10_devices: + housecode = device.get(CONF_HOUSECODE) + unitcode = device.get(CONF_UNITCODE) + x10_type = 'onoff' + steps = device.get(CONF_DIM_STEPS, 22) + if device.get(CONF_PLATFORM) == 'light': + x10_type = 'dimmable' + elif device.get(CONF_PLATFORM) == 'binary_sensor': + x10_type = 'sensor' + _LOGGER.debug("Adding X10 device to insteonplm: %s %d %s", + housecode, unitcode, x10_type) + device = plm.add_x10_device(housecode, + unitcode, + x10_type) + if device and hasattr(device.states[0x01], 'steps'): + device.states[0x01].steps = steps + hass.async_add_job(_register_services) return True @@ -219,6 +308,13 @@ class IPDB(object): IoLincSensor, LeakSensorDryWet) + from insteonplm.states.x10 import (X10DimmableSwitch, + X10OnOffSwitch, + X10OnOffSensor, + X10AllUnitsOffSensor, + X10AllLightsOnSensor, + X10AllLightsOffSensor) + self.states = [State(OnOffSwitch_OutletTop, 'switch'), State(OnOffSwitch_OutletBottom, 'switch'), State(OpenClosedRelay, 'switch'), @@ -231,7 +327,14 @@ class IPDB(object): State(VariableSensor, 'sensor'), State(DimmableSwitch_Fan, 'fan'), - State(DimmableSwitch, 'light')] + State(DimmableSwitch, 'light'), + + State(X10DimmableSwitch, 'light'), + State(X10OnOffSwitch, 'switch'), + State(X10OnOffSensor, 'binary_sensor'), + State(X10AllUnitsOffSensor, 'binary_sensor'), + State(X10AllLightsOnSensor, 'binary_sensor'), + State(X10AllLightsOffSensor, 'binary_sensor')] def __len__(self): """Return the number of INSTEON state types mapped to HA platforms.""" diff --git a/homeassistant/components/insteon_plm/services.yaml b/homeassistant/components/insteon_plm/services.yaml index 9ea53c10fbf..4d87d7881bf 100644 --- a/homeassistant/components/insteon_plm/services.yaml +++ b/homeassistant/components/insteon_plm/services.yaml @@ -30,3 +30,21 @@ print_all_link_database: example: 'light.1a2b3c' print_im_all_link_database: description: Print the All-Link Database for the INSTEON Modem (IM). +x10_all_units_off: + description: Send X10 All Units Off command + fields: + housecode: + description: X10 house code + example: c +x10_all_lights_on: + description: Send X10 All Lights On command + fields: + housecode: + description: X10 house code + example: c +x10_all_lights_off: + description: Send X10 All Lights Off command + fields: + housecode: + description: X10 house code + example: c diff --git a/homeassistant/components/switch/insteon_plm.py b/homeassistant/components/switch/insteon_plm.py index be562e9d909..42b4829f64e 100644 --- a/homeassistant/components/switch/insteon_plm.py +++ b/homeassistant/components/switch/insteon_plm.py @@ -30,7 +30,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): device.address.hex, device.states[state_key].name) new_entity = None - if state_name in ['lightOnOff', 'outletTopOnOff', 'outletBottomOnOff']: + if state_name in ['lightOnOff', 'outletTopOnOff', 'outletBottomOnOff', + 'x10OnOffSwitch']: new_entity = InsteonPLMSwitchDevice(device, state_key) elif state_name == 'openClosedRelay': new_entity = InsteonPLMOpenClosedDevice(device, state_key) diff --git a/requirements_all.txt b/requirements_all.txt index 5b83edac4a0..59e0f3a8bff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -463,7 +463,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.9.2 +insteonplm==0.10.0 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 From 8d22754a067b480be84c46c841492ae9daa5742a Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Thu, 21 Jun 2018 03:44:50 +0200 Subject: [PATCH 044/128] Update pyhomematic to 0.1.44 (#15069) * 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 29303b551e2..2e05f638afc 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.43'] +REQUIREMENTS = ['pyhomematic==0.1.44'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 59e0f3a8bff..84673eb5cb2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -846,7 +846,7 @@ pyhik==0.1.8 pyhiveapi==0.2.14 # homeassistant.components.homematic -pyhomematic==0.1.43 +pyhomematic==0.1.44 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.2.2 From 6cabbd2592df6df4b5197c19173b07491e9f76bb Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Wed, 20 Jun 2018 18:46:15 -0700 Subject: [PATCH 045/128] Update Neato Library And Reduce Cloud Calls (#15072) * Update Neato library to 0.0.6 and reduce the amount of calls to the cloud * Remove file commited in error * Lint --- homeassistant/components/camera/neato.py | 2 +- homeassistant/components/neato.py | 6 +++--- homeassistant/components/switch/neato.py | 3 +++ homeassistant/components/vacuum/neato.py | 4 +++- requirements_all.txt | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/camera/neato.py b/homeassistant/components/camera/neato.py index 33bd00caa6b..689129e1067 100644 --- a/homeassistant/components/camera/neato.py +++ b/homeassistant/components/camera/neato.py @@ -45,7 +45,7 @@ class NeatoCleaningMap(Camera): self.update() return self._image - @Throttle(timedelta(seconds=10)) + @Throttle(timedelta(seconds=60)) def update(self): """Check the contents of the map list.""" self.neato.update_robots() diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index 7402bb18843..c6a3dcf9c9a 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -17,8 +17,8 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.5.zip' - '#pybotvac==0.0.5'] +REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.6.zip' + '#pybotvac==0.0.6'] DOMAIN = 'neato' NEATO_ROBOTS = 'neato_robots' @@ -122,7 +122,7 @@ class NeatoHub(object): _LOGGER.error("Unable to connect to Neato API") return False - @Throttle(timedelta(seconds=1)) + @Throttle(timedelta(seconds=60)) def update_robots(self): """Update the robot states.""" _LOGGER.debug("Running HUB.update_robots %s", diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index a797abb47fc..1d149383f6f 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -5,10 +5,12 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.neato/ """ import logging +from datetime import timedelta import requests from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -50,6 +52,7 @@ class NeatoConnectedSwitch(ToggleEntity): self._schedule_state = None self._clean_state = None + @Throttle(timedelta(seconds=60)) def update(self): """Update the states of Neato switches.""" _LOGGER.debug("Running switch update") diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 9eba34cea32..128bece8494 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/vacuum.neato/ """ import logging - +from datetime import timedelta import requests from homeassistant.const import STATE_OFF, STATE_ON @@ -15,6 +15,7 @@ from homeassistant.components.vacuum import ( SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON) from homeassistant.components.neato import ( NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -62,6 +63,7 @@ class NeatoConnectedVacuum(VacuumDevice): self.clean_suspension_charge_count = None self.clean_suspension_time = None + @Throttle(timedelta(seconds=60)) def update(self): """Update the states of Neato Vacuums.""" _LOGGER.debug("Running Neato Vacuums update") diff --git a/requirements_all.txt b/requirements_all.txt index 84673eb5cb2..d47496fea59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,7 +430,7 @@ http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b89974819 httplib2==0.10.3 # homeassistant.components.neato -https://github.com/jabesq/pybotvac/archive/v0.0.5.zip#pybotvac==0.0.5 +https://github.com/jabesq/pybotvac/archive/v0.0.6.zip#pybotvac==0.0.6 # homeassistant.components.sensor.gtfs https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e2246267dd8.zip#pygtfs==0.1.3 From 8c2f0e3b3071ead3a7237e50b40661b530007de5 Mon Sep 17 00:00:00 2001 From: hanzoh Date: Thu, 21 Jun 2018 14:52:02 +0200 Subject: [PATCH 046/128] Homematic: Add optional port for resolvenames via JSON (#15029) * Add optional JSON port --- homeassistant/components/homematic/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 2e05f638afc..1428bbd3e56 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -148,6 +148,7 @@ CONF_PATH = 'path' CONF_CALLBACK_IP = 'callback_ip' CONF_CALLBACK_PORT = 'callback_port' CONF_RESOLVENAMES = 'resolvenames' +CONF_JSONPORT = 'jsonport' CONF_VARIABLES = 'variables' CONF_DEVICES = 'devices' CONF_PRIMARY = 'primary' @@ -155,6 +156,7 @@ CONF_PRIMARY = 'primary' DEFAULT_LOCAL_IP = '0.0.0.0' DEFAULT_LOCAL_PORT = 0 DEFAULT_RESOLVENAMES = False +DEFAULT_JSONPORT = 80 DEFAULT_PORT = 2001 DEFAULT_PATH = '' DEFAULT_USERNAME = 'Admin' @@ -178,6 +180,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES): vol.In(CONF_RESOLVENAMES_OPTIONS), + vol.Optional(CONF_JSONPORT, default=DEFAULT_JSONPORT): cv.port, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_CALLBACK_IP): cv.string, @@ -299,6 +302,7 @@ def setup(hass, config): 'port': rconfig.get(CONF_PORT), 'path': rconfig.get(CONF_PATH), 'resolvenames': rconfig.get(CONF_RESOLVENAMES), + 'jsonport': rconfig.get(CONF_JSONPORT), 'username': rconfig.get(CONF_USERNAME), 'password': rconfig.get(CONF_PASSWORD), 'callbackip': rconfig.get(CONF_CALLBACK_IP), From 4048ad36a85adebc543d5328dc9bb4e49f0309c9 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 21 Jun 2018 15:06:05 +0200 Subject: [PATCH 047/128] Add script to run monkeytype typing on test suite (#14440) * The monkeytype script takes an optional argument to specify a test module or directory to run. Otherwise the whole test suite will run. * Add monkeytype sqlite db to gitignore. --- .gitignore | 3 +++ script/monkeytype | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100755 script/monkeytype diff --git a/.gitignore b/.gitignore index bf49a1b61c1..c2b0d964a62 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,6 @@ desktop.ini # Secrets .lokalise_token + +# monkeytype +monkeytype.sqlite3 diff --git a/script/monkeytype b/script/monkeytype new file mode 100755 index 00000000000..dc1894c91ed --- /dev/null +++ b/script/monkeytype @@ -0,0 +1,25 @@ +#!/bin/sh +# Run monkeytype on test suite or optionally on a test module or directory. + +# Stop on errors +set -e + +cd "$(dirname "$0")/.." + +command -v pytest >/dev/null 2>&1 || { + echo >&2 "This script requires pytest but it's not installed." \ + "Aborting. Try: pip install pytest"; exit 1; } + +command -v monkeytype >/dev/null 2>&1 || { + echo >&2 "This script requires monkeytype but it's not installed." \ + "Aborting. Try: pip install monkeytype"; exit 1; } + +if [ $# -eq 0 ] + then + echo "Run monkeytype on test suite" + monkeytype run "`command -v pytest`" + exit +fi + +echo "Run monkeytype on tests in $1" +monkeytype run "`command -v pytest`" "$1" From b687de879c25c4101e3da12a624ce65923de9f61 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 14:57:08 -0400 Subject: [PATCH 048/128] Update frontend to 20180621.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 b2cac55bd77..9200f4d78f6 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180620.0'] +REQUIREMENTS = ['home-assistant-frontend==20180621.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index d47496fea59..62a73303899 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180620.0 +home-assistant-frontend==20180621.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e12ef3910a..69435adc83f 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==20180620.0 +home-assistant-frontend==20180621.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From bfc55137ea06a1a534088b566fb50c0abda871a0 Mon Sep 17 00:00:00 2001 From: Bob Clough Date: Thu, 21 Jun 2018 19:59:03 +0100 Subject: [PATCH 049/128] Fix MQTT Light with RGB and Brightness (#15053) * Fix MQTT Light with RGB and Brightness When an MQTT light is given an RGB and Brightness topic, the RGB is scaled by the brightness *as well* as the brightness being set This causes 255,0,0 at 50% brightness to be sent as 127,0,0 at 50% brightness, which ends up as 63,0,0 after the RGB bulb has applied its brightness scaling. Fixes the same issue in mqtt, mqtt-json and mqtt-template. Related Issue: #13725 * Add comment to mqtt_json as well --- homeassistant/components/light/mqtt.py | 11 +++++++++-- homeassistant/components/light/mqtt_json.py | 11 ++++++++--- homeassistant/components/light/mqtt_template.py | 11 +++++++++-- tests/components/light/test_mqtt.py | 14 +++++++------- tests/components/light/test_mqtt_json.py | 4 ++-- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 97a4cc8c137..c0e363f85d6 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -442,8 +442,15 @@ class MqttLight(MqttAvailability, Light): self._topic[CONF_RGB_COMMAND_TOPIC] is not None: hs_color = kwargs[ATTR_HS_COLOR] - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + + # If there's a brightness topic set, we don't want to scale the RGB + # values given using the brightness. + if self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else + 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE] diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 14f5ee7a9b9..705e106fdff 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -345,9 +345,14 @@ class MqttJson(MqttAvailability, Light): hs_color = kwargs[ATTR_HS_COLOR] message['color'] = {} if self._rgb: - brightness = kwargs.get( - ATTR_BRIGHTNESS, - self._brightness if self._brightness else 255) + # If there's a brightness topic set, we don't want to scale the + # RGB values given using the brightness. + if self._brightness is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, + self._brightness if self._brightness else 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) message['color']['r'] = rgb[0] diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index e32c13fc5b6..f6b3fbe8b70 100644 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -317,8 +317,15 @@ class MqttTemplate(MqttAvailability, Light): if ATTR_HS_COLOR in kwargs: hs_color = kwargs[ATTR_HS_COLOR] - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + + # If there's a brightness topic set, we don't want to scale the RGB + # values given using the brightness. + if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: + brightness = 255 + else: + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else + 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100) values['red'] = rgb[0] diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 8b51adb2187..49bcd8a73ec 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -523,24 +523,24 @@ class TestLightMQTT(unittest.TestCase): self.mock_publish.reset_mock() light.turn_on(self.hass, 'light.test', brightness=50, xy_color=[0.123, 0.123]) - light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75], + light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0], white_value=80) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 2, False), - mock.call('test_light_rgb/rgb/set', '50,50,50', 2, False), + mock.call('test_light_rgb/rgb/set', '255,128,0', 2, False), mock.call('test_light_rgb/brightness/set', 50, 2, False), mock.call('test_light_rgb/white_value/set', 80, 2, False), - mock.call('test_light_rgb/xy/set', '0.323,0.329', 2, False), + mock.call('test_light_rgb/xy/set', '0.14,0.131', 2, False), ], any_order=True) state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((255, 255, 255), state.attributes['rgb_color']) + self.assertEqual((255, 128, 0), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(80, state.attributes['white_value']) - self.assertEqual((0.323, 0.329), state.attributes['xy_color']) + self.assertEqual((0.611, 0.375), state.attributes['xy_color']) def test_sending_mqtt_rgb_command_with_template(self): """Test the sending of RGB command with template.""" @@ -808,11 +808,11 @@ class TestLightMQTT(unittest.TestCase): # Turn on w/ just a color to insure brightness gets # added and sent. - light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75]) + light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0]) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ - mock.call('test_light/rgb', '50,50,50', 0, False), + mock.call('test_light/rgb', '255,128,0', 0, False), mock.call('test_light/bright', 50, 0, False) ], any_order=True) diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 275fb42ede9..af560bff9c3 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -381,8 +381,8 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(50, message_json["brightness"]) self.assertEqual({ 'r': 0, - 'g': 50, - 'b': 4, + 'g': 255, + 'b': 21, }, message_json["color"]) self.assertEqual("ON", message_json["state"]) From 6781ecf159e35e91e8c2d12888a40da8386b4223 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:15:16 -0400 Subject: [PATCH 050/128] Bump frontend to 20180621.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 9200f4d78f6..d8497f9c790 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180621.0'] +REQUIREMENTS = ['home-assistant-frontend==20180621.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 62a73303899..6246ea9913e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.0 +home-assistant-frontend==20180621.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69435adc83f..938fd4976e8 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==20180621.0 +home-assistant-frontend==20180621.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 94eee6d0693538dea76d158c3f70aeddb9e8f4d1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jun 2018 17:38:44 -0400 Subject: [PATCH 051/128] Frontend bump to 20180621.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 d8497f9c790..89353b56098 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180621.1'] +REQUIREMENTS = ['home-assistant-frontend==20180621.2'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 6246ea9913e..14c882f32af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.1 +home-assistant-frontend==20180621.2 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 938fd4976e8..807509833ff 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==20180621.1 +home-assistant-frontend==20180621.2 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 0c01f3a0fe551dc5a4a2158f601eecdc2a50a63b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 22 Jun 2018 10:24:04 -0400 Subject: [PATCH 052/128] Update frontend to 20180622.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 89353b56098..9c9fdd137e2 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180621.2'] +REQUIREMENTS = ['home-assistant-frontend==20180622.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 14c882f32af..27ecc784f59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180621.2 +home-assistant-frontend==20180622.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 807509833ff..52e7e3e07b1 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==20180621.2 +home-assistant-frontend==20180622.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From c419cbb46f2af51bb83043ada4e9b25b64d95942 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 22 Jun 2018 12:46:45 -0400 Subject: [PATCH 053/128] Bump frontend to 20180622.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 9c9fdd137e2..3d2231ab43b 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180622.0'] +REQUIREMENTS = ['home-assistant-frontend==20180622.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 27ecc784f59..008ed05143f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.0 +home-assistant-frontend==20180622.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 52e7e3e07b1..45f47bbe514 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==20180622.0 +home-assistant-frontend==20180622.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 91962e2681dde1b23612df06633b16aa0867c950 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 23 Jun 2018 13:22:48 -0600 Subject: [PATCH 054/128] Fix socket bug with Yi in 0.72 (#15109) * Fixes BrokenPipeError exceptions with Yi (#15108) * Make sure to close the socket --- homeassistant/components/camera/yi.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py index 868c5afb447..93f526c2b96 100644 --- a/homeassistant/components/camera/yi.py +++ b/homeassistant/components/camera/yi.py @@ -53,7 +53,6 @@ class YiCamera(Camera): """Initialize.""" super().__init__() self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS) - self._ftp = None self._last_image = None self._last_url = None self._manager = hass.data[DATA_FFMPEG] @@ -64,8 +63,6 @@ class YiCamera(Camera): self.user = config[CONF_USERNAME] self.passwd = config[CONF_PASSWORD] - hass.async_add_job(self._connect_to_client) - @property def brand(self): """Camera brand.""" @@ -76,38 +73,35 @@ class YiCamera(Camera): """Return the name of this camera.""" return self._name - async def _connect_to_client(self): - """Attempt to establish a connection via FTP.""" + async def _get_latest_video_url(self): + """Retrieve the latest video file from the customized Yi FTP server.""" from aioftp import Client, StatusCodeError ftp = Client() try: await ftp.connect(self.host) await ftp.login(self.user, self.passwd) - self._ftp = ftp except StatusCodeError as err: raise PlatformNotReady(err) - async def _get_latest_video_url(self): - """Retrieve the latest video file from the customized Yi FTP server.""" - from aioftp import StatusCodeError - try: - await self._ftp.change_directory(self.path) + await ftp.change_directory(self.path) dirs = [] - for path, attrs in await self._ftp.list(): + for path, attrs in await ftp.list(): if attrs['type'] == 'dir' and '.' not in str(path): dirs.append(path) latest_dir = dirs[-1] - await self._ftp.change_directory(latest_dir) + await ftp.change_directory(latest_dir) videos = [] - for path, _ in await self._ftp.list(): + for path, _ in await ftp.list(): videos.append(path) if not videos: _LOGGER.info('Video folder "%s" empty; delaying', latest_dir) return None + await ftp.quit() + return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format( self.user, self.passwd, self.host, self.port, self.path, latest_dir, videos[-1]) From 96d5684a89f1b6fbf313de3da5f1e50107bb2d53 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sun, 24 Jun 2018 12:06:25 +0300 Subject: [PATCH 055/128] Switch to pypi version of pybotvac (#15115) --- homeassistant/components/neato.py | 3 +-- requirements_all.txt | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index c6a3dcf9c9a..6d14a6f3c4d 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -17,8 +17,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.6.zip' - '#pybotvac==0.0.6'] +REQUIREMENTS = ['pybotvac==0.0.7'] DOMAIN = 'neato' NEATO_ROBOTS = 'neato_robots' diff --git a/requirements_all.txt b/requirements_all.txt index 008ed05143f..6dd581fc299 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -429,9 +429,6 @@ http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b89974819 # homeassistant.components.remember_the_milk httplib2==0.10.3 -# homeassistant.components.neato -https://github.com/jabesq/pybotvac/archive/v0.0.6.zip#pybotvac==0.0.6 - # homeassistant.components.sensor.gtfs https://github.com/robbiet480/pygtfs/archive/00546724e4bbcb3053110d844ca44e2246267dd8.zip#pygtfs==0.1.3 @@ -763,6 +760,9 @@ pyblackbird==0.5 # homeassistant.components.device_tracker.bluetooth_tracker # pybluez==0.22 +# homeassistant.components.neato +pybotvac==0.0.7 + # homeassistant.components.media_player.channels pychannels==1.0.0 From 9de7034d0e4e4f6cb6a2e8f20c9d0577aed319f6 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sun, 24 Jun 2018 07:36:27 -0400 Subject: [PATCH 056/128] Added attribute attribution to Digital Ocean component (#15114) --- homeassistant/components/binary_sensor/digital_ocean.py | 4 +++- homeassistant/components/digital_ocean.py | 1 + homeassistant/components/switch/digital_ocean.py | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/digital_ocean.py b/homeassistant/components/binary_sensor/digital_ocean.py index 140c84358c7..1eb86d4eb82 100644 --- a/homeassistant/components/binary_sensor/digital_ocean.py +++ b/homeassistant/components/binary_sensor/digital_ocean.py @@ -14,7 +14,8 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.digital_ocean import ( CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, - ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN) + ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) +from homeassistant.const import ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -75,6 +76,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice): def device_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_CREATED_AT: self.data.created_at, ATTR_DROPLET_ID: self.data.id, ATTR_DROPLET_NAME: self.data.name, diff --git a/homeassistant/components/digital_ocean.py b/homeassistant/components/digital_ocean.py index bd03fb01975..a0f50842649 100644 --- a/homeassistant/components/digital_ocean.py +++ b/homeassistant/components/digital_ocean.py @@ -27,6 +27,7 @@ ATTR_MEMORY = 'memory' ATTR_REGION = 'region' ATTR_VCPUS = 'vcpus' +CONF_ATTRIBUTION = 'Data provided by Digital Ocean' CONF_DROPLETS = 'droplets' DATA_DIGITAL_OCEAN = 'data_do' diff --git a/homeassistant/components/switch/digital_ocean.py b/homeassistant/components/switch/digital_ocean.py index 081eea80e2d..12a6aabb170 100644 --- a/homeassistant/components/switch/digital_ocean.py +++ b/homeassistant/components/switch/digital_ocean.py @@ -13,7 +13,8 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.components.digital_ocean import ( CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME, ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, - ATTR_REGION, ATTR_VCPUS, DATA_DIGITAL_OCEAN) + ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) +from homeassistant.const import ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -69,6 +70,7 @@ class DigitalOceanSwitch(SwitchDevice): def device_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_CREATED_AT: self.data.created_at, ATTR_DROPLET_ID: self.data.id, ATTR_DROPLET_NAME: self.data.name, From 6064932e2e5f4351fe091cd1140c4ca0aeb746ad Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 24 Jun 2018 11:04:31 -0600 Subject: [PATCH 057/128] Make Pollen.com platform async (#14963) * Most of the work in place * Final touches * Small style updates * Owner-requested changes * Member-requested changes --- homeassistant/components/sensor/pollen.py | 427 ++++++++++------------ requirements_all.txt | 2 +- 2 files changed, 190 insertions(+), 239 deletions(-) diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index 1ef5a27cf3d..838358fcfca 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -13,17 +13,17 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_STATE, CONF_MONITORED_CONDITIONS -) + ATTR_ATTRIBUTION, ATTR_STATE, CONF_MONITORED_CONDITIONS) +from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle, slugify +from homeassistant.util import Throttle -REQUIREMENTS = ['pypollencom==1.1.2'] +REQUIREMENTS = ['pypollencom==2.1.0'] _LOGGER = logging.getLogger(__name__) -ATTR_ALLERGEN_GENUS = 'primary_allergen_genus' -ATTR_ALLERGEN_NAME = 'primary_allergen_name' -ATTR_ALLERGEN_TYPE = 'primary_allergen_type' +ATTR_ALLERGEN_GENUS = 'allergen_genus' +ATTR_ALLERGEN_NAME = 'allergen_name' +ATTR_ALLERGEN_TYPE = 'allergen_type' ATTR_CITY = 'city' ATTR_OUTLOOK = 'outlook' ATTR_RATING = 'rating' @@ -34,53 +34,30 @@ ATTR_ZIP_CODE = 'zip_code' CONF_ZIP_CODE = 'zip_code' DEFAULT_ATTRIBUTION = 'Data provided by IQVIA™' +DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) -MIN_TIME_UPDATE_AVERAGES = timedelta(hours=12) -MIN_TIME_UPDATE_INDICES = timedelta(minutes=10) +TYPE_ALLERGY_FORECAST = 'allergy_average_forecasted' +TYPE_ALLERGY_HISTORIC = 'allergy_average_historical' +TYPE_ALLERGY_INDEX = 'allergy_index' +TYPE_ALLERGY_OUTLOOK = 'allergy_outlook' +TYPE_ALLERGY_TODAY = 'allergy_index_today' +TYPE_ALLERGY_TOMORROW = 'allergy_index_tomorrow' +TYPE_ALLERGY_YESTERDAY = 'allergy_index_yesterday' +TYPE_DISEASE_FORECAST = 'disease_average_forecasted' -CONDITIONS = { - 'allergy_average_forecasted': ( - 'Allergy Index: Forecasted Average', - 'AllergyAverageSensor', - 'allergy_average_data', - {'data_attr': 'extended_data'}, - 'mdi:flower' - ), - 'allergy_average_historical': ( - 'Allergy Index: Historical Average', - 'AllergyAverageSensor', - 'allergy_average_data', - {'data_attr': 'historic_data'}, - 'mdi:flower' - ), - 'allergy_index_today': ( - 'Allergy Index: Today', - 'AllergyIndexSensor', - 'allergy_index_data', - {'key': 'Today'}, - 'mdi:flower' - ), - 'allergy_index_tomorrow': ( - 'Allergy Index: Tomorrow', - 'AllergyIndexSensor', - 'allergy_index_data', - {'key': 'Tomorrow'}, - 'mdi:flower' - ), - 'allergy_index_yesterday': ( - 'Allergy Index: Yesterday', - 'AllergyIndexSensor', - 'allergy_index_data', - {'key': 'Yesterday'}, - 'mdi:flower' - ), - 'disease_average_forecasted': ( - 'Cold & Flu: Forecasted Average', - 'AllergyAverageSensor', - 'disease_average_data', - {'data_attr': 'extended_data'}, - 'mdi:snowflake' - ) +SENSORS = { + TYPE_ALLERGY_FORECAST: ( + 'Allergy Index: Forecasted Average', None, 'mdi:flower', 'index'), + TYPE_ALLERGY_HISTORIC: ( + 'Allergy Index: Historical Average', None, 'mdi:flower', 'index'), + TYPE_ALLERGY_TODAY: ( + 'Allergy Index: Today', TYPE_ALLERGY_INDEX, 'mdi:flower', 'index'), + TYPE_ALLERGY_TOMORROW: ( + 'Allergy Index: Tomorrow', TYPE_ALLERGY_INDEX, 'mdi:flower', 'index'), + TYPE_ALLERGY_YESTERDAY: ( + 'Allergy Index: Yesterday', TYPE_ALLERGY_INDEX, 'mdi:flower', 'index'), + TYPE_DISEASE_FORECAST: ( + 'Cold & Flu: Forecasted Average', None, 'mdi:snowflake', 'index') } RATING_MAPPING = [{ @@ -105,69 +82,69 @@ RATING_MAPPING = [{ 'maximum': 12 }] +TREND_FLAT = 'Flat' +TREND_INCREASING = 'Increasing' +TREND_SUBSIDING = 'Subsiding' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ZIP_CODE): str, - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(CONDITIONS)]), + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) }) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Configure the platform and add the sensors.""" from pypollencom import Client - _LOGGER.debug('Configuration data: %s', config) + websession = aiohttp_client.async_get_clientsession(hass) - client = Client(config[CONF_ZIP_CODE]) - datas = { - 'allergy_average_data': AllergyAveragesData(client), - 'allergy_index_data': AllergyIndexData(client), - 'disease_average_data': DiseaseData(client) - } - classes = { - 'AllergyAverageSensor': AllergyAverageSensor, - 'AllergyIndexSensor': AllergyIndexSensor - } + data = PollenComData( + Client(config[CONF_ZIP_CODE], websession), + config[CONF_MONITORED_CONDITIONS]) - for data in datas.values(): - data.update() + await data.async_update() sensors = [] - for condition in config[CONF_MONITORED_CONDITIONS]: - name, sensor_class, data_key, params, icon = CONDITIONS[condition] - sensors.append(classes[sensor_class]( - datas[data_key], - params, - name, - icon, - config[CONF_ZIP_CODE] - )) + for kind in config[CONF_MONITORED_CONDITIONS]: + name, category, icon, unit = SENSORS[kind] + sensors.append( + PollencomSensor( + data, config[CONF_ZIP_CODE], kind, category, name, icon, unit)) - add_devices(sensors, True) + async_add_devices(sensors, True) -def calculate_trend(list_of_nums): - """Calculate the most common rating as a trend.""" +def calculate_average_rating(indices): + """Calculate the human-friendly historical allergy average.""" ratings = list( - r['label'] for n in list_of_nums - for r in RATING_MAPPING + r['label'] for n in indices for r in RATING_MAPPING if r['minimum'] <= n <= r['maximum']) return max(set(ratings), key=ratings.count) -class BaseSensor(Entity): - """Define a base class for all of our sensors.""" +class PollencomSensor(Entity): + """Define a Pollen.com sensor.""" - def __init__(self, data, data_params, name, icon, unique_id): + def __init__(self, pollencom, zip_code, kind, category, name, icon, unit): """Initialize the sensor.""" self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._category = category self._icon = icon self._name = name - self._data_params = data_params self._state = None - self._unit = None - self._unique_id = unique_id - self.data = data + self._type = kind + self._unit = unit + self._zip_code = zip_code + self.pollencom = pollencom + + @property + def available(self): + """Return True if entity is available.""" + return bool( + self.pollencom.data.get(self._type) + or self.pollencom.data.get(self._category)) @property def device_state_attributes(self): @@ -192,187 +169,161 @@ class BaseSensor(Entity): @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_{1}'.format(self._unique_id, slugify(self._name)) + return '{0}_{1}'.format(self._zip_code, self._type) @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit - -class AllergyAverageSensor(BaseSensor): - """Define a sensor to show allergy average information.""" - - def update(self): - """Update the status of the sensor.""" - self.data.update() - - try: - data_attr = getattr(self.data, self._data_params['data_attr']) - indices = [p['Index'] for p in data_attr['Location']['periods']] - self._attrs[ATTR_TREND] = calculate_trend(indices) - except KeyError: - _LOGGER.error("Pollen.com API didn't return any data") + async def async_update(self): + """Update the sensor.""" + await self.pollencom.async_update() + if not self.pollencom.data: return - try: - self._attrs[ATTR_CITY] = data_attr['Location']['City'].title() - self._attrs[ATTR_STATE] = data_attr['Location']['State'] - self._attrs[ATTR_ZIP_CODE] = data_attr['Location']['ZIP'] - except KeyError: - _LOGGER.debug('Location data not included in API response') - self._attrs[ATTR_CITY] = None - self._attrs[ATTR_STATE] = None - self._attrs[ATTR_ZIP_CODE] = None + if self._category: + data = self.pollencom.data[self._category]['Location'] + else: + data = self.pollencom.data[self._type]['Location'] + indices = [p['Index'] for p in data['periods']] average = round(mean(indices), 1) [rating] = [ i['label'] for i in RATING_MAPPING if i['minimum'] <= average <= i['maximum'] ] - self._attrs[ATTR_RATING] = rating + slope = (data['periods'][-1]['Index'] - data['periods'][-2]['Index']) + trend = TREND_FLAT + if slope > 0: + trend = TREND_INCREASING + elif slope < 0: + trend = TREND_SUBSIDING - self._state = average - self._unit = 'index' + if self._type == TYPE_ALLERGY_FORECAST: + outlook = self.pollencom.data[TYPE_ALLERGY_OUTLOOK] - -class AllergyIndexSensor(BaseSensor): - """Define a sensor to show allergy index information.""" - - def update(self): - """Update the status of the sensor.""" - self.data.update() - - try: - location_data = self.data.current_data['Location'] - [period] = [ - p for p in location_data['periods'] - if p['Type'] == self._data_params['key'] - ] + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_OUTLOOK: outlook['Outlook'], + ATTR_RATING: rating, + ATTR_SEASON: outlook['Season'].title(), + ATTR_STATE: data['State'], + ATTR_TREND: outlook['Trend'].title(), + ATTR_ZIP_CODE: data['ZIP'] + }) + self._state = average + elif self._type == TYPE_ALLERGY_HISTORIC: + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_RATING: calculate_average_rating(indices), + ATTR_STATE: data['State'], + ATTR_TREND: trend, + ATTR_ZIP_CODE: data['ZIP'] + }) + self._state = average + elif self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ALLERGY_YESTERDAY): + key = self._type.split('_')[-1].title() + [period] = [p for p in data['periods'] if p['Type'] == key] [rating] = [ i['label'] for i in RATING_MAPPING if i['minimum'] <= period['Index'] <= i['maximum'] ] - for i in range(3): - index = i + 1 - try: - data = period['Triggers'][i] - self._attrs['{0}_{1}'.format( - ATTR_ALLERGEN_GENUS, index)] = data['Genus'] - self._attrs['{0}_{1}'.format( - ATTR_ALLERGEN_NAME, index)] = data['Name'] - self._attrs['{0}_{1}'.format( - ATTR_ALLERGEN_TYPE, index)] = data['PlantType'] - except IndexError: - self._attrs['{0}_{1}'.format( - ATTR_ALLERGEN_GENUS, index)] = None - self._attrs['{0}_{1}'.format( - ATTR_ALLERGEN_NAME, index)] = None - self._attrs['{0}_{1}'.format( - ATTR_ALLERGEN_TYPE, index)] = None + for idx, attrs in enumerate(period['Triggers']): + index = idx + 1 + self._attrs.update({ + '{0}_{1}'.format(ATTR_ALLERGEN_GENUS, index): + attrs['Genus'], + '{0}_{1}'.format(ATTR_ALLERGEN_NAME, index): + attrs['Name'], + '{0}_{1}'.format(ATTR_ALLERGEN_TYPE, index): + attrs['PlantType'], + }) - self._attrs[ATTR_RATING] = rating - - except KeyError: - _LOGGER.error("Pollen.com API didn't return any data") - return - - try: - self._attrs[ATTR_CITY] = location_data['City'].title() - self._attrs[ATTR_STATE] = location_data['State'] - self._attrs[ATTR_ZIP_CODE] = location_data['ZIP'] - except KeyError: - _LOGGER.debug('Location data not included in API response') - self._attrs[ATTR_CITY] = None - self._attrs[ATTR_STATE] = None - self._attrs[ATTR_ZIP_CODE] = None - - try: - self._attrs[ATTR_OUTLOOK] = self.data.outlook_data['Outlook'] - except KeyError: - _LOGGER.debug('Outlook data not included in API response') - self._attrs[ATTR_OUTLOOK] = None - - try: - self._attrs[ATTR_SEASON] = self.data.outlook_data['Season'] - except KeyError: - _LOGGER.debug('Season data not included in API response') - self._attrs[ATTR_SEASON] = None - - try: - self._attrs[ATTR_TREND] = self.data.outlook_data['Trend'].title() - except KeyError: - _LOGGER.debug('Trend data not included in API response') - self._attrs[ATTR_TREND] = None - - self._state = period['Index'] - self._unit = 'index' + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_RATING: rating, + ATTR_STATE: data['State'], + ATTR_ZIP_CODE: data['ZIP'] + }) + self._state = period['Index'] + elif self._type == TYPE_DISEASE_FORECAST: + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_RATING: rating, + ATTR_STATE: data['State'], + ATTR_TREND: trend, + ATTR_ZIP_CODE: data['ZIP'] + }) + self._state = average -class DataBase(object): - """Define a generic data object.""" +class PollenComData(object): + """Define a data object to retrieve info from Pollen.com.""" - def __init__(self, client): + def __init__(self, client, sensor_types): """Initialize.""" self._client = client + self._sensor_types = sensor_types + self.data = {} - def _get_client_data(self, module, operation): - """Get data from a particular point in the API.""" - from pypollencom.exceptions import HTTPError + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update Pollen.com data.""" + from pypollencom.errors import InvalidZipError, PollenComError + + # Pollen.com requires a bit more complicated error handling, given that + # it sometimes has parts (but not the whole thing) go down: + # + # 1. If `InvalidZipError` is thrown, quit everything immediately. + # 2. If an individual request throws any other error, try the others. - data = {} try: - data = getattr(getattr(self._client, module), operation)() - _LOGGER.debug('Received "%s_%s" data: %s', module, operation, data) - except HTTPError as exc: - _LOGGER.error('An error occurred while retrieving data') - _LOGGER.debug(exc) + if TYPE_ALLERGY_FORECAST in self._sensor_types: + try: + data = await self._client.allergens.extended() + self.data[TYPE_ALLERGY_FORECAST] = data + except PollenComError as err: + _LOGGER.error('Unable to get allergy forecast: %s', err) + self.data[TYPE_ALLERGY_FORECAST] = {} - return data + try: + data = await self._client.allergens.outlook() + self.data[TYPE_ALLERGY_OUTLOOK] = data + except PollenComError as err: + _LOGGER.error('Unable to get allergy outlook: %s', err) + self.data[TYPE_ALLERGY_OUTLOOK] = {} + if TYPE_ALLERGY_HISTORIC in self._sensor_types: + try: + data = await self._client.allergens.historic() + self.data[TYPE_ALLERGY_HISTORIC] = data + except PollenComError as err: + _LOGGER.error('Unable to get allergy history: %s', err) + self.data[TYPE_ALLERGY_HISTORIC] = {} -class AllergyAveragesData(DataBase): - """Define an object to averages on future and historical allergy data.""" + if all(s in self._sensor_types + for s in [TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ALLERGY_YESTERDAY]): + try: + data = await self._client.allergens.current() + self.data[TYPE_ALLERGY_INDEX] = data + except PollenComError as err: + _LOGGER.error('Unable to get current allergies: %s', err) + self.data[TYPE_ALLERGY_TODAY] = {} - def __init__(self, client): - """Initialize.""" - super().__init__(client) - self.extended_data = None - self.historic_data = None + if TYPE_DISEASE_FORECAST in self._sensor_types: + try: + data = await self._client.disease.extended() + self.data[TYPE_DISEASE_FORECAST] = data + except PollenComError as err: + _LOGGER.error('Unable to get disease forecast: %s', err) + self.data[TYPE_DISEASE_FORECAST] = {} - @Throttle(MIN_TIME_UPDATE_AVERAGES) - def update(self): - """Update with new data.""" - self.extended_data = self._get_client_data('allergens', 'extended') - self.historic_data = self._get_client_data('allergens', 'historic') - - -class AllergyIndexData(DataBase): - """Define an object to retrieve current allergy index info.""" - - def __init__(self, client): - """Initialize.""" - super().__init__(client) - self.current_data = None - self.outlook_data = None - - @Throttle(MIN_TIME_UPDATE_INDICES) - def update(self): - """Update with new index data.""" - self.current_data = self._get_client_data('allergens', 'current') - self.outlook_data = self._get_client_data('allergens', 'outlook') - - -class DiseaseData(DataBase): - """Define an object to retrieve current disease index info.""" - - def __init__(self, client): - """Initialize.""" - super().__init__(client) - self.extended_data = None - - @Throttle(MIN_TIME_UPDATE_INDICES) - def update(self): - """Update with new cold/flu data.""" - self.extended_data = self._get_client_data('disease', 'extended') + _LOGGER.debug('New data retrieved: %s', self.data) + except InvalidZipError: + _LOGGER.error( + 'Cannot retrieve data for ZIP code: %s', self._client.zip_code) + self.data = {} diff --git a/requirements_all.txt b/requirements_all.txt index 6dd581fc299..6117119b949 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -954,7 +954,7 @@ pyotp==2.2.6 pyowm==2.8.0 # homeassistant.components.sensor.pollen -pypollencom==1.1.2 +pypollencom==2.1.0 # homeassistant.components.qwikswitch pyqwikswitch==0.8 From 5a71a22fb90f5320630ba2bed14a0a10f8cbe1c0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 24 Jun 2018 23:48:59 +0200 Subject: [PATCH 058/128] deCONZ small improvements (#15128) * Make sure that bridge id is available for config entry * Fix so deconz reports proper color values * Bump dependency to v39 --- homeassistant/components/deconz/__init__.py | 2 +- homeassistant/components/deconz/config_flow.py | 9 +++------ homeassistant/components/light/deconz.py | 8 +++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 850645225d0..4fa89f8cfd3 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -22,7 +22,7 @@ from .const import ( CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==38'] +REQUIREMENTS = ['pydeconz==39'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 27fb6987f8c..b67d32508be 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -163,9 +163,6 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): if CONF_API_KEY not in import_config: return await self.async_step_link() - self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True - self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = True - return self.async_create_entry( - title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], - data=self.deconz_config - ) + user_input = {CONF_ALLOW_CLIP_SENSOR: True, + CONF_ALLOW_DECONZ_GROUPS: True} + return await self.async_step_options(user_input=user_input) diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index a4593a72617..05907ea86ee 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -101,9 +101,11 @@ class DeconzLight(Light): return self._light.ct @property - def xy_color(self): - """Return the XY color value.""" - return self._light.xy + def hs_color(self): + """Return the hs color value.""" + if self._light.colormode in ('xy', 'hs') and self._light.xy: + return color_util.color_xy_to_hs(*self._light.xy) + return None @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index 6117119b949..777309442f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -786,7 +786,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==38 +pydeconz==39 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45f47bbe514..6813378b12f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -133,7 +133,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==38 +pydeconz==39 # homeassistant.components.zwave pydispatcher==2.0.5 From 021d08a9c46bf3ff32f4c6fbc632757006611a33 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 24 Jun 2018 19:09:08 -0600 Subject: [PATCH 059/128] Make sure Yi utilizes existing event loop (#15131) --- homeassistant/components/camera/yi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py index 93f526c2b96..b575a705f98 100644 --- a/homeassistant/components/camera/yi.py +++ b/homeassistant/components/camera/yi.py @@ -77,7 +77,7 @@ class YiCamera(Camera): """Retrieve the latest video file from the customized Yi FTP server.""" from aioftp import Client, StatusCodeError - ftp = Client() + ftp = Client(loop=self.hass.loop) try: await ftp.connect(self.host) await ftp.login(self.user, self.passwd) From 05924a286812ff7d7ab111e432412b95dbc3ad19 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 25 Jun 2018 06:46:55 -0500 Subject: [PATCH 060/128] Bump insteonplm version to 0.11.2 (#15133) * Bump insteonplm version to 0.11.2 * Gratuitous commit to force travis again. * Reverse change made 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 b2f7c8b6655..8197b45c28d 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.10.0'] +REQUIREMENTS = ['insteonplm==0.11.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 777309442f3..74ff9286803 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -460,7 +460,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.10.0 +insteonplm==0.11.2 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 From 3893d8a87612ed807f77d52db1d863eb7cefad83 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 25 Jun 2018 13:58:16 +0200 Subject: [PATCH 061/128] Reorganize mysensors (#15123) * Move mysensors.py to package * Move mysensors component to package * Split code into multiple modules. * Update coveragerc --- .coveragerc | 2 +- .../components/binary_sensor/mysensors.py | 3 +- homeassistant/components/climate/mysensors.py | 2 +- homeassistant/components/cover/mysensors.py | 2 +- .../components/device_tracker/mysensors.py | 4 +- homeassistant/components/light/mysensors.py | 2 +- homeassistant/components/mysensors.py | 705 ------------------ .../components/mysensors/__init__.py | 167 +++++ homeassistant/components/mysensors/const.py | 138 ++++ homeassistant/components/mysensors/device.py | 109 +++ homeassistant/components/mysensors/gateway.py | 328 ++++++++ homeassistant/components/notify/mysensors.py | 2 +- homeassistant/components/sensor/mysensors.py | 2 +- homeassistant/components/switch/mysensors.py | 2 +- 14 files changed, 753 insertions(+), 715 deletions(-) delete mode 100644 homeassistant/components/mysensors.py create mode 100644 homeassistant/components/mysensors/__init__.py create mode 100644 homeassistant/components/mysensors/const.py create mode 100644 homeassistant/components/mysensors/device.py create mode 100644 homeassistant/components/mysensors/gateway.py diff --git a/.coveragerc b/.coveragerc index d059d62b5f3..90b0a7f475d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -192,7 +192,7 @@ omit = homeassistant/components/mychevy.py homeassistant/components/*/mychevy.py - homeassistant/components/mysensors.py + homeassistant/components/mysensors/* homeassistant/components/*/mysensors.py homeassistant/components/neato.py diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 21443021193..abb19129d52 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -29,7 +29,8 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice): +class MySensorsBinarySensor( + mysensors.device.MySensorsEntity, BinarySensorDevice): """Representation of a MySensors Binary Sensor child node.""" @property diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 9fab56c61ac..37ae29fdf81 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -39,7 +39,7 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): +class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): """Representation of a MySensors HVAC.""" @property diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index 3f8eb054710..c815cf44df2 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -17,7 +17,7 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): +class MySensorsCover(mysensors.device.MySensorsEntity, CoverDevice): """Representation of the value of a MySensors Cover child node.""" @property diff --git a/homeassistant/components/device_tracker/mysensors.py b/homeassistant/components/device_tracker/mysensors.py index b0d29bf0566..49d3f3207ba 100644 --- a/homeassistant/components/device_tracker/mysensors.py +++ b/homeassistant/components/device_tracker/mysensors.py @@ -23,13 +23,13 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): id(device.gateway), device.node_id, device.child_id, device.value_type) async_dispatcher_connect( - hass, mysensors.SIGNAL_CALLBACK.format(*dev_id), + hass, mysensors.const.SIGNAL_CALLBACK.format(*dev_id), device.async_update_callback) return True -class MySensorsDeviceScanner(mysensors.MySensorsDevice): +class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): """Represent a MySensors scanner.""" def __init__(self, async_see, *args): diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 55387288d7f..4139abd40fa 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -28,7 +28,7 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsLight(mysensors.MySensorsEntity, Light): +class MySensorsLight(mysensors.device.MySensorsEntity, Light): """Representation of a MySensors Light child node.""" def __init__(self, *args): diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py deleted file mode 100644 index 1e7e252bd9d..00000000000 --- a/homeassistant/components/mysensors.py +++ /dev/null @@ -1,705 +0,0 @@ -""" -Connect to a MySensors gateway via pymysensors API. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mysensors/ -""" -import asyncio -from collections import defaultdict -import logging -import os -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) -from homeassistant.core import callback -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, async_dispatcher_send) -from homeassistant.helpers.entity import Entity -from homeassistant.setup import async_setup_component - -REQUIREMENTS = ['pymysensors==0.14.0'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_CHILD_ID = 'child_id' -ATTR_DESCRIPTION = 'description' -ATTR_DEVICE = 'device' -ATTR_DEVICES = 'devices' -ATTR_NODE_ID = 'node_id' - -CONF_BAUD_RATE = 'baud_rate' -CONF_DEBUG = 'debug' -CONF_DEVICE = 'device' -CONF_GATEWAYS = 'gateways' -CONF_PERSISTENCE = 'persistence' -CONF_PERSISTENCE_FILE = 'persistence_file' -CONF_RETAIN = 'retain' -CONF_TCP_PORT = 'tcp_port' -CONF_TOPIC_IN_PREFIX = 'topic_in_prefix' -CONF_TOPIC_OUT_PREFIX = 'topic_out_prefix' -CONF_VERSION = 'version' - -CONF_NODES = 'nodes' -CONF_NODE_NAME = 'name' - -DEFAULT_BAUD_RATE = 115200 -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_{}_{}_{}_{}' -TYPE = 'type' - - -def is_socket_address(value): - """Validate that value is a valid address.""" - try: - socket.getaddrinfo(value, None) - return value - except OSError: - raise vol.Invalid('Device is not a valid domain name or ip address') - - -def has_parent_dir(value): - """Validate that value is in an existing directory which is writeable.""" - parent = os.path.dirname(os.path.realpath(value)) - is_dir_writable = os.path.isdir(parent) and os.access(parent, os.W_OK) - if not is_dir_writable: - raise vol.Invalid( - '{} directory does not exist or is not writeable'.format(parent)) - return value - - -def has_all_unique_files(value): - """Validate that all persistence files are unique and set if any is set.""" - persistence_files = [ - gateway.get(CONF_PERSISTENCE_FILE) for gateway in value] - if None in persistence_files and any( - name is not None for name in persistence_files): - raise vol.Invalid( - 'persistence file name of all devices must be set if any is set') - if not all(name is None for name in persistence_files): - schema = vol.Schema(vol.Unique()) - schema(persistence_files) - return value - - -def is_persistence_file(value): - """Validate that persistence file path ends in either .pickle or .json.""" - if value.endswith(('.json', '.pickle')): - return value - else: - raise vol.Invalid( - '{} does not end in either `.json` or `.pickle`'.format(value)) - - -def is_serial_port(value): - """Validate that value is a windows serial port or a unix device.""" - if sys.platform.startswith('win'): - ports = ('COM{}'.format(idx + 1) for idx in range(256)) - if value in ports: - return value - else: - raise vol.Invalid('{} is not a serial port'.format(value)) - else: - return cv.isdevice(value) - - -def deprecated(key): - """Mark key as deprecated in configuration.""" - def validator(config): - """Check if key is in config, log warning and remove key.""" - if key not in config: - return config - _LOGGER.warning( - '%s option for %s is deprecated. Please remove %s from your ' - 'configuration file', key, DOMAIN, key) - config.pop(key) - return config - return validator - - -NODE_SCHEMA = vol.Schema({ - cv.positive_int: { - vol.Required(CONF_NODE_NAME): cv.string - } -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema(vol.All(deprecated(CONF_DEBUG), { - vol.Required(CONF_GATEWAYS): vol.All( - cv.ensure_list, has_all_unique_files, - [{ - vol.Required(CONF_DEVICE): - vol.Any(MQTT_COMPONENT, is_socket_address, is_serial_port), - vol.Optional(CONF_PERSISTENCE_FILE): - vol.All(cv.string, is_persistence_file, has_parent_dir), - vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): - cv.positive_int, - vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port, - vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic, - vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic, - vol.Optional(CONF_NODES, default={}): NODE_SCHEMA, - }] - ), - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, - vol.Optional(CONF_RETAIN, default=True): cv.boolean, - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, - })) -}, extra=vol.ALLOW_EXTRA) - - -# MySensors const schemas -BINARY_SENSOR_SCHEMA = {PLATFORM: 'binary_sensor', TYPE: 'V_TRIPPED'} -CLIMATE_SCHEMA = {PLATFORM: 'climate', TYPE: 'V_HVAC_FLOW_STATE'} -LIGHT_DIMMER_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_DIMMER', - SCHEMA: {'V_DIMMER': cv.string, 'V_LIGHT': cv.string}} -LIGHT_PERCENTAGE_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_PERCENTAGE', - SCHEMA: {'V_PERCENTAGE': cv.string, 'V_STATUS': cv.string}} -LIGHT_RGB_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_RGB', SCHEMA: { - 'V_RGB': cv.string, 'V_STATUS': cv.string}} -LIGHT_RGBW_SCHEMA = { - PLATFORM: 'light', TYPE: 'V_RGBW', SCHEMA: { - 'V_RGBW': cv.string, 'V_STATUS': cv.string}} -NOTIFY_SCHEMA = {PLATFORM: 'notify', TYPE: 'V_TEXT'} -DEVICE_TRACKER_SCHEMA = {PLATFORM: 'device_tracker', TYPE: 'V_POSITION'} -DUST_SCHEMA = [ - {PLATFORM: 'sensor', TYPE: 'V_DUST_LEVEL'}, - {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}] -SWITCH_LIGHT_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_LIGHT'} -SWITCH_STATUS_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_STATUS'} -MYSENSORS_CONST_SCHEMA = { - 'S_DOOR': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_MOTION': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_SMOKE': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_SPRINKLER': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_STATUS'}], - 'S_WATER_LEAK': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_SOUND': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, - {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_VIBRATION': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, - {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_MOISTURE': [ - BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, - {PLATFORM: 'switch', TYPE: 'V_ARMED'}], - 'S_HVAC': [CLIMATE_SCHEMA], - 'S_COVER': [ - {PLATFORM: 'cover', TYPE: 'V_DIMMER'}, - {PLATFORM: 'cover', TYPE: 'V_PERCENTAGE'}, - {PLATFORM: 'cover', TYPE: 'V_LIGHT'}, - {PLATFORM: 'cover', TYPE: 'V_STATUS'}], - 'S_DIMMER': [LIGHT_DIMMER_SCHEMA, LIGHT_PERCENTAGE_SCHEMA], - 'S_RGB_LIGHT': [LIGHT_RGB_SCHEMA], - 'S_RGBW_LIGHT': [LIGHT_RGBW_SCHEMA], - 'S_INFO': [NOTIFY_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_TEXT'}], - 'S_GPS': [ - DEVICE_TRACKER_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_POSITION'}], - 'S_TEMP': [{PLATFORM: 'sensor', TYPE: 'V_TEMP'}], - 'S_HUM': [{PLATFORM: 'sensor', TYPE: 'V_HUM'}], - 'S_BARO': [ - {PLATFORM: 'sensor', TYPE: 'V_PRESSURE'}, - {PLATFORM: 'sensor', TYPE: 'V_FORECAST'}], - 'S_WIND': [ - {PLATFORM: 'sensor', TYPE: 'V_WIND'}, - {PLATFORM: 'sensor', TYPE: 'V_GUST'}, - {PLATFORM: 'sensor', TYPE: 'V_DIRECTION'}], - 'S_RAIN': [ - {PLATFORM: 'sensor', TYPE: 'V_RAIN'}, - {PLATFORM: 'sensor', TYPE: 'V_RAINRATE'}], - 'S_UV': [{PLATFORM: 'sensor', TYPE: 'V_UV'}], - 'S_WEIGHT': [ - {PLATFORM: 'sensor', TYPE: 'V_WEIGHT'}, - {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], - 'S_POWER': [ - {PLATFORM: 'sensor', TYPE: 'V_WATT'}, - {PLATFORM: 'sensor', TYPE: 'V_KWH'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR'}, - {PLATFORM: 'sensor', TYPE: 'V_VA'}, - {PLATFORM: 'sensor', TYPE: 'V_POWER_FACTOR'}], - 'S_DISTANCE': [{PLATFORM: 'sensor', TYPE: 'V_DISTANCE'}], - 'S_LIGHT_LEVEL': [ - {PLATFORM: 'sensor', TYPE: 'V_LIGHT_LEVEL'}, - {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}], - 'S_IR': [ - {PLATFORM: 'sensor', TYPE: 'V_IR_RECEIVE'}, - {PLATFORM: 'switch', TYPE: 'V_IR_SEND', - SCHEMA: {'V_IR_SEND': cv.string, 'V_LIGHT': cv.string}}], - 'S_WATER': [ - {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, - {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], - 'S_CUSTOM': [ - {PLATFORM: 'sensor', TYPE: 'V_VAR1'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR2'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR3'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR4'}, - {PLATFORM: 'sensor', TYPE: 'V_VAR5'}, - {PLATFORM: 'sensor', TYPE: 'V_CUSTOM'}], - 'S_SCENE_CONTROLLER': [ - {PLATFORM: 'sensor', TYPE: 'V_SCENE_ON'}, - {PLATFORM: 'sensor', TYPE: 'V_SCENE_OFF'}], - 'S_COLOR_SENSOR': [{PLATFORM: 'sensor', TYPE: 'V_RGB'}], - 'S_MULTIMETER': [ - {PLATFORM: 'sensor', TYPE: 'V_VOLTAGE'}, - {PLATFORM: 'sensor', TYPE: 'V_CURRENT'}, - {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], - 'S_GAS': [ - {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, - {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], - 'S_WATER_QUALITY': [ - {PLATFORM: 'sensor', TYPE: 'V_TEMP'}, - {PLATFORM: 'sensor', TYPE: 'V_PH'}, - {PLATFORM: 'sensor', TYPE: 'V_ORP'}, - {PLATFORM: 'sensor', TYPE: 'V_EC'}, - {PLATFORM: 'switch', TYPE: 'V_STATUS'}], - 'S_AIR_QUALITY': DUST_SCHEMA, - 'S_DUST': DUST_SCHEMA, - 'S_LIGHT': [SWITCH_LIGHT_SCHEMA], - 'S_BINARY': [SWITCH_STATUS_SCHEMA], - 'S_LOCK': [{PLATFORM: 'switch', TYPE: 'V_LOCK_STATUS'}], -} - - -async def async_setup(hass, config): - """Set up the MySensors component.""" - import mysensors.mysensors as mysensors - - version = config[DOMAIN].get(CONF_VERSION) - persistence = config[DOMAIN].get(CONF_PERSISTENCE) - - async def setup_gateway( - device, persistence_file, baud_rate, tcp_port, in_prefix, - out_prefix): - """Return gateway after setup of the gateway.""" - if device == MQTT_COMPONENT: - if not await async_setup_component(hass, MQTT_COMPONENT, config): - return None - mqtt = hass.components.mqtt - retain = config[DOMAIN].get(CONF_RETAIN) - - def pub_callback(topic, payload, qos, retain): - """Call MQTT publish function.""" - mqtt.async_publish(topic, payload, qos, retain) - - def sub_callback(topic, sub_cb, qos): - """Call MQTT subscribe function.""" - @callback - def internal_callback(*args): - """Call callback.""" - sub_cb(*args) - - hass.async_add_job( - mqtt.async_subscribe(topic, internal_callback, qos)) - - gateway = mysensors.AsyncMQTTGateway( - pub_callback, sub_callback, in_prefix=in_prefix, - out_prefix=out_prefix, retain=retain, loop=hass.loop, - event_callback=None, persistence=persistence, - persistence_file=persistence_file, - protocol_version=version) - else: - try: - await hass.async_add_job(is_serial_port, device) - gateway = mysensors.AsyncSerialGateway( - device, baud=baud_rate, loop=hass.loop, - event_callback=None, persistence=persistence, - persistence_file=persistence_file, - protocol_version=version) - except vol.Invalid: - gateway = mysensors.AsyncTCPGateway( - device, port=tcp_port, loop=hass.loop, event_callback=None, - persistence=persistence, persistence_file=persistence_file, - protocol_version=version) - gateway.metric = hass.config.units.is_metric - gateway.optimistic = config[DOMAIN].get(CONF_OPTIMISTIC) - gateway.device = device - gateway.event_callback = gw_callback_factory(hass) - if persistence: - await gateway.start_persistence() - - return gateway - - # Setup all devices from config - gateways = {} - conf_gateways = config[DOMAIN][CONF_GATEWAYS] - - for index, gway in enumerate(conf_gateways): - device = gway[CONF_DEVICE] - persistence_file = gway.get( - CONF_PERSISTENCE_FILE, - hass.config.path('mysensors{}.pickle'.format(index + 1))) - baud_rate = gway.get(CONF_BAUD_RATE) - tcp_port = gway.get(CONF_TCP_PORT) - in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '') - out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '') - gateway = await setup_gateway( - device, persistence_file, baud_rate, tcp_port, in_prefix, - out_prefix) - if gateway is not None: - gateway.nodes_config = gway.get(CONF_NODES) - gateways[id(gateway)] = gateway - - if not gateways: - _LOGGER.error( - "No devices could be setup as gateways, check your configuration") - return False - - hass.data[MYSENSORS_GATEWAYS] = gateways - - hass.async_add_job(finish_setup(hass, gateways)) - - return True - - -async def finish_setup(hass, gateways): - """Load any persistent devices and platforms and start gateway.""" - discover_tasks = [] - start_tasks = [] - for gateway in gateways.values(): - discover_tasks.append(discover_persistent_devices(hass, gateway)) - start_tasks.append(gw_start(hass, gateway)) - if discover_tasks: - # Make sure all devices and platforms are loaded before gateway start. - await asyncio.wait(discover_tasks, loop=hass.loop) - if start_tasks: - await asyncio.wait(start_tasks, loop=hass.loop) - - -async def gw_start(hass, gateway): - """Start the gateway.""" - @callback - def gw_stop(event): - """Trigger to stop the gateway.""" - hass.async_add_job(gateway.stop()) - - 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): - """Validate that a child has the correct values according to schema. - - Return a dict of platform with a list of device ids for validated devices. - """ - validated = defaultdict(list) - - if not child.values: - _LOGGER.debug( - "No child values for node %s child %s", node_id, child.id) - return validated - if gateway.sensors[node_id].sketch_name is None: - _LOGGER.debug("Node %s is missing sketch name", node_id) - return validated - pres = gateway.const.Presentation - set_req = gateway.const.SetReq - s_name = next( - (member.name for member in pres if member.value == child.type), None) - if s_name not in MYSENSORS_CONST_SCHEMA: - _LOGGER.warning("Child type %s is not supported", s_name) - return validated - child_schemas = MYSENSORS_CONST_SCHEMA[s_name] - - def msg(name): - """Return a message for an invalid schema.""" - return "{} requires value_type {}".format( - pres(child.type).name, set_req[name].name) - - for schema in child_schemas: - platform = schema[PLATFORM] - v_name = schema[TYPE] - value_type = next( - (member.value for member in set_req if member.name == v_name), - None) - if value_type is None: - continue - _child_schema = child.get_schema(gateway.protocol_version) - vol_schema = _child_schema.extend( - {vol.Required(set_req[key].value, msg=msg(key)): - _child_schema.schema.get(set_req[key].value, val) - for key, val in schema.get(SCHEMA, {v_name: cv.string}).items()}, - extra=vol.ALLOW_EXTRA) - try: - vol_schema(child.values) - except vol.Invalid as exc: - level = (logging.WARNING if value_type in child.values - else logging.DEBUG) - _LOGGER.log( - level, - "Invalid values: %s: %s platform: node %s child %s: %s", - child.values, platform, node_id, child.id, exc) - continue - dev_id = id(gateway), node_id, child.id, value_type - validated[platform].append(dev_id) - return validated - - -@callback -def discover_mysensors_platform(hass, platform, new_devices): - """Discover a MySensors platform.""" - task = hass.async_add_job(discovery.async_load_platform( - hass, platform, DOMAIN, - {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN})) - return task - - -async def discover_persistent_devices(hass, gateway): - """Discover platforms for devices loaded via persistence file.""" - tasks = [] - new_devices = defaultdict(list) - for node_id in gateway.sensors: - node = gateway.sensors[node_id] - for child in node.children.values(): - validated = validate_child(gateway, node_id, child) - for platform, dev_ids in validated.items(): - new_devices[platform].extend(dev_ids) - for platform, dev_ids in new_devices.items(): - tasks.append(discover_mysensors_platform(hass, platform, dev_ids)) - if tasks: - await asyncio.wait(tasks, loop=hass.loop) - - -def get_mysensors_devices(hass, domain): - """Return MySensors devices for a platform.""" - if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data: - hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] = {} - return hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] - - -def gw_callback_factory(hass): - """Return a new callback for the gateway.""" - @callback - def mysensors_callback(msg): - """Handle messages from a MySensors gateway.""" - start = timer() - _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: - _LOGGER.debug("Not a child update for node %s", msg.node_id) - return - - signals = [] - - # Update all platforms for the device via dispatcher. - # Add/update entity if schema validates to true. - validated = validate_child(msg.gateway, msg.node_id, child) - for platform, dev_ids in validated.items(): - devices = get_mysensors_devices(hass, platform) - new_dev_ids = [] - for dev_id in dev_ids: - if dev_id in devices: - signals.append(SIGNAL_CALLBACK.format(*dev_id)) - else: - new_dev_ids.append(dev_id) - if new_dev_ids: - discover_mysensors_platform(hass, platform, new_dev_ids) - for signal in set(signals): - # Only one signal per device is needed. - # A device can have multiple platforms, ie multiple schemas. - # FOR LATER: Add timer to not signal if another update comes in. - async_dispatcher_send(hass, signal) - end = timer() - if end - start > 0.1: - _LOGGER.debug( - "Callback for node %s child %s took %.3f seconds", - msg.node_id, msg.child_id, end - start) - return mysensors_callback - - -def get_mysensors_name(gateway, node_id, child_id): - """Return a name for a node child.""" - node_name = '{} {}'.format( - gateway.sensors[node_id].sketch_name, node_id) - node_name = next( - (node[CONF_NODE_NAME] for conf_id, node in gateway.nodes_config.items() - if node.get(CONF_NODE_NAME) is not None and conf_id == node_id), - node_name) - return '{} {}'.format(node_name, child_id) - - -def get_mysensors_gateway(hass, gateway_id): - """Return MySensors gateway.""" - if MYSENSORS_GATEWAYS not in hass.data: - hass.data[MYSENSORS_GATEWAYS] = {} - gateways = hass.data.get(MYSENSORS_GATEWAYS) - return gateways.get(gateway_id) - - -@callback -def setup_mysensors_platform( - hass, domain, discovery_info, device_class, device_args=None, - async_add_devices=None): - """Set up a MySensors platform.""" - # Only act if called via mysensors by discovery event. - # Otherwise gateway is not setup. - if not discovery_info: - return - if device_args is None: - device_args = () - new_devices = [] - new_dev_ids = discovery_info[ATTR_DEVICES] - for dev_id in new_dev_ids: - devices = get_mysensors_devices(hass, domain) - if dev_id in devices: - continue - gateway_id, node_id, child_id, value_type = dev_id - gateway = get_mysensors_gateway(hass, gateway_id) - if not gateway: - continue - device_class_copy = device_class - if isinstance(device_class, dict): - child = gateway.sensors[node_id].children[child_id] - s_type = gateway.const.Presentation(child.type).name - device_class_copy = device_class[s_type] - name = get_mysensors_name(gateway, node_id, child_id) - - args_copy = (*device_args, gateway, node_id, child_id, name, - value_type) - devices[dev_id] = device_class_copy(*args_copy) - new_devices.append(devices[dev_id]) - if new_devices: - _LOGGER.info("Adding new devices: %s", new_devices) - if async_add_devices is not None: - async_add_devices(new_devices, True) - return new_devices - - -class MySensorsDevice(object): - """Representation of a MySensors device.""" - - def __init__(self, gateway, node_id, child_id, name, value_type): - """Set up the MySensors device.""" - self.gateway = gateway - self.node_id = node_id - self.child_id = child_id - self._name = name - self.value_type = value_type - child = gateway.sensors[node_id].children[child_id] - self.child_type = child.type - self._values = {} - - @property - def name(self): - """Return the name of this entity.""" - return self._name - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - attr = { - ATTR_BATTERY_LEVEL: node.battery_level, - ATTR_CHILD_ID: self.child_id, - ATTR_DESCRIPTION: child.description, - ATTR_DEVICE: self.gateway.device, - ATTR_NODE_ID: self.node_id, - } - - set_req = self.gateway.const.SetReq - - for value_type, value in self._values.items(): - attr[set_req(value_type).name] = value - - return attr - - async def async_update(self): - """Update the controller with the latest value from a sensor.""" - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - set_req = self.gateway.const.SetReq - for value_type, value in child.values.items(): - _LOGGER.debug( - "Entity update: %s: value_type %s, value = %s", - self._name, value_type, value) - if value_type in (set_req.V_ARMED, set_req.V_LIGHT, - set_req.V_LOCK_STATUS, set_req.V_TRIPPED): - self._values[value_type] = ( - STATE_ON if int(value) == 1 else STATE_OFF) - elif value_type == set_req.V_DIMMER: - self._values[value_type] = int(value) - else: - self._values[value_type] = value - - -class MySensorsEntity(MySensorsDevice, Entity): - """Representation of a MySensors entity.""" - - @property - def should_poll(self): - """Return the polling state. The gateway pushes its states.""" - return False - - @property - def available(self): - """Return true if entity is available.""" - return self.value_type in self._values - - @callback - def async_update_callback(self): - """Update the entity.""" - self.async_schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Register update callback.""" - dev_id = id(self.gateway), self.node_id, self.child_id, self.value_type - async_dispatcher_connect( - self.hass, SIGNAL_CALLBACK.format(*dev_id), - self.async_update_callback) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py new file mode 100644 index 00000000000..3aa8e82911e --- /dev/null +++ b/homeassistant/components/mysensors/__init__.py @@ -0,0 +1,167 @@ +""" +Connect to a MySensors gateway via pymysensors API. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/mysensors/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.mqtt import ( + valid_publish_topic, valid_subscribe_topic) +from homeassistant.const import CONF_OPTIMISTIC +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from .const import ( + ATTR_DEVICES, CONF_BAUD_RATE, CONF_DEVICE, CONF_GATEWAYS, + CONF_NODES, CONF_PERSISTENCE, CONF_PERSISTENCE_FILE, CONF_RETAIN, + CONF_TCP_PORT, CONF_TOPIC_IN_PREFIX, CONF_TOPIC_OUT_PREFIX, CONF_VERSION, + DOMAIN, MYSENSORS_GATEWAYS) +from .device import get_mysensors_devices +from .gateway import get_mysensors_gateway, setup_gateways, finish_setup + +REQUIREMENTS = ['pymysensors==0.14.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_DEBUG = 'debug' +CONF_NODE_NAME = 'name' + +DEFAULT_BAUD_RATE = 115200 +DEFAULT_TCP_PORT = 5003 +DEFAULT_VERSION = '1.4' + + +def has_all_unique_files(value): + """Validate that all persistence files are unique and set if any is set.""" + persistence_files = [ + gateway.get(CONF_PERSISTENCE_FILE) for gateway in value] + if None in persistence_files and any( + name is not None for name in persistence_files): + raise vol.Invalid( + 'persistence file name of all devices must be set if any is set') + if not all(name is None for name in persistence_files): + schema = vol.Schema(vol.Unique()) + schema(persistence_files) + return value + + +def is_persistence_file(value): + """Validate that persistence file path ends in either .pickle or .json.""" + if value.endswith(('.json', '.pickle')): + return value + else: + raise vol.Invalid( + '{} does not end in either `.json` or `.pickle`'.format(value)) + + +def deprecated(key): + """Mark key as deprecated in configuration.""" + def validator(config): + """Check if key is in config, log warning and remove key.""" + if key not in config: + return config + _LOGGER.warning( + '%s option for %s is deprecated. Please remove %s from your ' + 'configuration file', key, DOMAIN, key) + config.pop(key) + return config + return validator + + +NODE_SCHEMA = vol.Schema({ + cv.positive_int: { + vol.Required(CONF_NODE_NAME): cv.string + } +}) + +GATEWAY_SCHEMA = { + vol.Required(CONF_DEVICE): cv.string, + vol.Optional(CONF_PERSISTENCE_FILE): + vol.All(cv.string, is_persistence_file), + vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): + cv.positive_int, + vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port, + vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic, + vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic, + vol.Optional(CONF_NODES, default={}): NODE_SCHEMA, +} + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema(vol.All(deprecated(CONF_DEBUG), { + vol.Required(CONF_GATEWAYS): vol.All( + cv.ensure_list, has_all_unique_files, [GATEWAY_SCHEMA]), + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, + vol.Optional(CONF_RETAIN, default=True): cv.boolean, + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, + })) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the MySensors component.""" + gateways = await setup_gateways(hass, config) + + if not gateways: + _LOGGER.error( + "No devices could be setup as gateways, check your configuration") + return False + + hass.data[MYSENSORS_GATEWAYS] = gateways + + hass.async_add_job(finish_setup(hass, gateways)) + + return True + + +def _get_mysensors_name(gateway, node_id, child_id): + """Return a name for a node child.""" + node_name = '{} {}'.format( + gateway.sensors[node_id].sketch_name, node_id) + node_name = next( + (node[CONF_NODE_NAME] for conf_id, node in gateway.nodes_config.items() + if node.get(CONF_NODE_NAME) is not None and conf_id == node_id), + node_name) + return '{} {}'.format(node_name, child_id) + + +@callback +def setup_mysensors_platform( + hass, domain, discovery_info, device_class, device_args=None, + async_add_devices=None): + """Set up a MySensors platform.""" + # Only act if called via MySensors by discovery event. + # Otherwise gateway is not setup. + if not discovery_info: + return + if device_args is None: + device_args = () + new_devices = [] + new_dev_ids = discovery_info[ATTR_DEVICES] + for dev_id in new_dev_ids: + devices = get_mysensors_devices(hass, domain) + if dev_id in devices: + continue + gateway_id, node_id, child_id, value_type = dev_id + gateway = get_mysensors_gateway(hass, gateway_id) + if not gateway: + continue + device_class_copy = device_class + if isinstance(device_class, dict): + child = gateway.sensors[node_id].children[child_id] + s_type = gateway.const.Presentation(child.type).name + device_class_copy = device_class[s_type] + name = _get_mysensors_name(gateway, node_id, child_id) + + args_copy = (*device_args, gateway, node_id, child_id, name, + value_type) + devices[dev_id] = device_class_copy(*args_copy) + new_devices.append(devices[dev_id]) + if new_devices: + _LOGGER.info("Adding new devices: %s", new_devices) + if async_add_devices is not None: + async_add_devices(new_devices, True) + return new_devices diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py new file mode 100644 index 00000000000..4f9718a39db --- /dev/null +++ b/homeassistant/components/mysensors/const.py @@ -0,0 +1,138 @@ +"""MySensors constants.""" +import homeassistant.helpers.config_validation as cv + +ATTR_DEVICES = 'devices' + +CONF_BAUD_RATE = 'baud_rate' +CONF_DEVICE = 'device' +CONF_GATEWAYS = 'gateways' +CONF_NODES = 'nodes' +CONF_PERSISTENCE = 'persistence' +CONF_PERSISTENCE_FILE = 'persistence_file' +CONF_RETAIN = 'retain' +CONF_TCP_PORT = 'tcp_port' +CONF_TOPIC_IN_PREFIX = 'topic_in_prefix' +CONF_TOPIC_OUT_PREFIX = 'topic_out_prefix' +CONF_VERSION = 'version' + +DOMAIN = 'mysensors' +MYSENSORS_GATEWAYS = 'mysensors_gateways' +PLATFORM = 'platform' +SCHEMA = 'schema' +SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}' +TYPE = 'type' + +# MySensors const schemas +BINARY_SENSOR_SCHEMA = {PLATFORM: 'binary_sensor', TYPE: 'V_TRIPPED'} +CLIMATE_SCHEMA = {PLATFORM: 'climate', TYPE: 'V_HVAC_FLOW_STATE'} +LIGHT_DIMMER_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_DIMMER', + SCHEMA: {'V_DIMMER': cv.string, 'V_LIGHT': cv.string}} +LIGHT_PERCENTAGE_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_PERCENTAGE', + SCHEMA: {'V_PERCENTAGE': cv.string, 'V_STATUS': cv.string}} +LIGHT_RGB_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_RGB', SCHEMA: { + 'V_RGB': cv.string, 'V_STATUS': cv.string}} +LIGHT_RGBW_SCHEMA = { + PLATFORM: 'light', TYPE: 'V_RGBW', SCHEMA: { + 'V_RGBW': cv.string, 'V_STATUS': cv.string}} +NOTIFY_SCHEMA = {PLATFORM: 'notify', TYPE: 'V_TEXT'} +DEVICE_TRACKER_SCHEMA = {PLATFORM: 'device_tracker', TYPE: 'V_POSITION'} +DUST_SCHEMA = [ + {PLATFORM: 'sensor', TYPE: 'V_DUST_LEVEL'}, + {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}] +SWITCH_LIGHT_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_LIGHT'} +SWITCH_STATUS_SCHEMA = {PLATFORM: 'switch', TYPE: 'V_STATUS'} +MYSENSORS_CONST_SCHEMA = { + 'S_DOOR': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_MOTION': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_SMOKE': [BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_SPRINKLER': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_STATUS'}], + 'S_WATER_LEAK': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_SOUND': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, + {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_VIBRATION': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, + {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_MOISTURE': [ + BINARY_SENSOR_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}, + {PLATFORM: 'switch', TYPE: 'V_ARMED'}], + 'S_HVAC': [CLIMATE_SCHEMA], + 'S_COVER': [ + {PLATFORM: 'cover', TYPE: 'V_DIMMER'}, + {PLATFORM: 'cover', TYPE: 'V_PERCENTAGE'}, + {PLATFORM: 'cover', TYPE: 'V_LIGHT'}, + {PLATFORM: 'cover', TYPE: 'V_STATUS'}], + 'S_DIMMER': [LIGHT_DIMMER_SCHEMA, LIGHT_PERCENTAGE_SCHEMA], + 'S_RGB_LIGHT': [LIGHT_RGB_SCHEMA], + 'S_RGBW_LIGHT': [LIGHT_RGBW_SCHEMA], + 'S_INFO': [NOTIFY_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_TEXT'}], + 'S_GPS': [ + DEVICE_TRACKER_SCHEMA, {PLATFORM: 'sensor', TYPE: 'V_POSITION'}], + 'S_TEMP': [{PLATFORM: 'sensor', TYPE: 'V_TEMP'}], + 'S_HUM': [{PLATFORM: 'sensor', TYPE: 'V_HUM'}], + 'S_BARO': [ + {PLATFORM: 'sensor', TYPE: 'V_PRESSURE'}, + {PLATFORM: 'sensor', TYPE: 'V_FORECAST'}], + 'S_WIND': [ + {PLATFORM: 'sensor', TYPE: 'V_WIND'}, + {PLATFORM: 'sensor', TYPE: 'V_GUST'}, + {PLATFORM: 'sensor', TYPE: 'V_DIRECTION'}], + 'S_RAIN': [ + {PLATFORM: 'sensor', TYPE: 'V_RAIN'}, + {PLATFORM: 'sensor', TYPE: 'V_RAINRATE'}], + 'S_UV': [{PLATFORM: 'sensor', TYPE: 'V_UV'}], + 'S_WEIGHT': [ + {PLATFORM: 'sensor', TYPE: 'V_WEIGHT'}, + {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], + 'S_POWER': [ + {PLATFORM: 'sensor', TYPE: 'V_WATT'}, + {PLATFORM: 'sensor', TYPE: 'V_KWH'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR'}, + {PLATFORM: 'sensor', TYPE: 'V_VA'}, + {PLATFORM: 'sensor', TYPE: 'V_POWER_FACTOR'}], + 'S_DISTANCE': [{PLATFORM: 'sensor', TYPE: 'V_DISTANCE'}], + 'S_LIGHT_LEVEL': [ + {PLATFORM: 'sensor', TYPE: 'V_LIGHT_LEVEL'}, + {PLATFORM: 'sensor', TYPE: 'V_LEVEL'}], + 'S_IR': [ + {PLATFORM: 'sensor', TYPE: 'V_IR_RECEIVE'}, + {PLATFORM: 'switch', TYPE: 'V_IR_SEND', + SCHEMA: {'V_IR_SEND': cv.string, 'V_LIGHT': cv.string}}], + 'S_WATER': [ + {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, + {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], + 'S_CUSTOM': [ + {PLATFORM: 'sensor', TYPE: 'V_VAR1'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR2'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR3'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR4'}, + {PLATFORM: 'sensor', TYPE: 'V_VAR5'}, + {PLATFORM: 'sensor', TYPE: 'V_CUSTOM'}], + 'S_SCENE_CONTROLLER': [ + {PLATFORM: 'sensor', TYPE: 'V_SCENE_ON'}, + {PLATFORM: 'sensor', TYPE: 'V_SCENE_OFF'}], + 'S_COLOR_SENSOR': [{PLATFORM: 'sensor', TYPE: 'V_RGB'}], + 'S_MULTIMETER': [ + {PLATFORM: 'sensor', TYPE: 'V_VOLTAGE'}, + {PLATFORM: 'sensor', TYPE: 'V_CURRENT'}, + {PLATFORM: 'sensor', TYPE: 'V_IMPEDANCE'}], + 'S_GAS': [ + {PLATFORM: 'sensor', TYPE: 'V_FLOW'}, + {PLATFORM: 'sensor', TYPE: 'V_VOLUME'}], + 'S_WATER_QUALITY': [ + {PLATFORM: 'sensor', TYPE: 'V_TEMP'}, + {PLATFORM: 'sensor', TYPE: 'V_PH'}, + {PLATFORM: 'sensor', TYPE: 'V_ORP'}, + {PLATFORM: 'sensor', TYPE: 'V_EC'}, + {PLATFORM: 'switch', TYPE: 'V_STATUS'}], + 'S_AIR_QUALITY': DUST_SCHEMA, + 'S_DUST': DUST_SCHEMA, + 'S_LIGHT': [SWITCH_LIGHT_SCHEMA], + 'S_BINARY': [SWITCH_STATUS_SCHEMA], + 'S_LOCK': [{PLATFORM: 'switch', TYPE: 'V_LOCK_STATUS'}], +} diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py new file mode 100644 index 00000000000..b0770f90c1d --- /dev/null +++ b/homeassistant/components/mysensors/device.py @@ -0,0 +1,109 @@ +"""Handle MySensors devices.""" +import logging + +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import SIGNAL_CALLBACK + +_LOGGER = logging.getLogger(__name__) + +ATTR_CHILD_ID = 'child_id' +ATTR_DESCRIPTION = 'description' +ATTR_DEVICE = 'device' +ATTR_NODE_ID = 'node_id' +MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}' + + +def get_mysensors_devices(hass, domain): + """Return MySensors devices for a platform.""" + if MYSENSORS_PLATFORM_DEVICES.format(domain) not in hass.data: + hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] = {} + return hass.data[MYSENSORS_PLATFORM_DEVICES.format(domain)] + + +class MySensorsDevice(object): + """Representation of a MySensors device.""" + + def __init__(self, gateway, node_id, child_id, name, value_type): + """Set up the MySensors device.""" + self.gateway = gateway + self.node_id = node_id + self.child_id = child_id + self._name = name + self.value_type = value_type + child = gateway.sensors[node_id].children[child_id] + self.child_type = child.type + self._values = {} + + @property + def name(self): + """Return the name of this entity.""" + return self._name + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + attr = { + ATTR_BATTERY_LEVEL: node.battery_level, + ATTR_CHILD_ID: self.child_id, + ATTR_DESCRIPTION: child.description, + ATTR_DEVICE: self.gateway.device, + ATTR_NODE_ID: self.node_id, + } + + set_req = self.gateway.const.SetReq + + for value_type, value in self._values.items(): + attr[set_req(value_type).name] = value + + return attr + + async def async_update(self): + """Update the controller with the latest value from a sensor.""" + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + set_req = self.gateway.const.SetReq + for value_type, value in child.values.items(): + _LOGGER.debug( + "Entity update: %s: value_type %s, value = %s", + self._name, value_type, value) + if value_type in (set_req.V_ARMED, set_req.V_LIGHT, + set_req.V_LOCK_STATUS, set_req.V_TRIPPED): + self._values[value_type] = ( + STATE_ON if int(value) == 1 else STATE_OFF) + elif value_type == set_req.V_DIMMER: + self._values[value_type] = int(value) + else: + self._values[value_type] = value + + +class MySensorsEntity(MySensorsDevice, Entity): + """Representation of a MySensors entity.""" + + @property + def should_poll(self): + """Return the polling state. The gateway pushes its states.""" + return False + + @property + def available(self): + """Return true if entity is available.""" + return self.value_type in self._values + + @callback + def async_update_callback(self): + """Update the entity.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register update callback.""" + dev_id = id(self.gateway), self.node_id, self.child_id, self.value_type + async_dispatcher_connect( + self.hass, SIGNAL_CALLBACK.format(*dev_id), + self.async_update_callback) diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py new file mode 100644 index 00000000000..a7719a80d99 --- /dev/null +++ b/homeassistant/components/mysensors/gateway.py @@ -0,0 +1,328 @@ +"""Handle MySensors gateways.""" +import asyncio +from collections import defaultdict +import logging +import socket +import sys +from timeit import default_timer as timer + +import async_timeout +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.setup import async_setup_component + +from .const import ( + ATTR_DEVICES, CONF_BAUD_RATE, CONF_DEVICE, CONF_GATEWAYS, CONF_NODES, + CONF_PERSISTENCE, CONF_PERSISTENCE_FILE, CONF_RETAIN, CONF_TCP_PORT, + CONF_TOPIC_IN_PREFIX, CONF_TOPIC_OUT_PREFIX, CONF_VERSION, DOMAIN, + MYSENSORS_CONST_SCHEMA, MYSENSORS_GATEWAYS, PLATFORM, SCHEMA, + SIGNAL_CALLBACK, TYPE) +from .device import get_mysensors_devices + +_LOGGER = logging.getLogger(__name__) + +GATEWAY_READY_TIMEOUT = 15.0 +MQTT_COMPONENT = 'mqtt' +MYSENSORS_GATEWAY_READY = 'mysensors_gateway_ready_{}' + + +def is_serial_port(value): + """Validate that value is a windows serial port or a unix device.""" + if sys.platform.startswith('win'): + ports = ('COM{}'.format(idx + 1) for idx in range(256)) + if value in ports: + return value + else: + raise vol.Invalid('{} is not a serial port'.format(value)) + else: + return cv.isdevice(value) + + +def is_socket_address(value): + """Validate that value is a valid address.""" + try: + socket.getaddrinfo(value, None) + return value + except OSError: + raise vol.Invalid('Device is not a valid domain name or ip address') + + +def get_mysensors_gateway(hass, gateway_id): + """Return MySensors gateway.""" + if MYSENSORS_GATEWAYS not in hass.data: + hass.data[MYSENSORS_GATEWAYS] = {} + gateways = hass.data.get(MYSENSORS_GATEWAYS) + return gateways.get(gateway_id) + + +async def setup_gateways(hass, config): + """Set up all gateways.""" + conf = config[DOMAIN] + gateways = {} + + for index, gateway_conf in enumerate(conf[CONF_GATEWAYS]): + persistence_file = gateway_conf.get( + CONF_PERSISTENCE_FILE, + hass.config.path('mysensors{}.pickle'.format(index + 1))) + ready_gateway = await _get_gateway( + hass, config, gateway_conf, persistence_file) + if ready_gateway is not None: + gateways[id(ready_gateway)] = ready_gateway + + return gateways + + +async def _get_gateway(hass, config, gateway_conf, persistence_file): + """Return gateway after setup of the gateway.""" + import mysensors.mysensors as mysensors + + conf = config[DOMAIN] + persistence = conf[CONF_PERSISTENCE] + version = conf[CONF_VERSION] + device = gateway_conf[CONF_DEVICE] + baud_rate = gateway_conf[CONF_BAUD_RATE] + tcp_port = gateway_conf[CONF_TCP_PORT] + in_prefix = gateway_conf.get(CONF_TOPIC_IN_PREFIX, '') + out_prefix = gateway_conf.get(CONF_TOPIC_OUT_PREFIX, '') + + if device == MQTT_COMPONENT: + if not await async_setup_component(hass, MQTT_COMPONENT, config): + return None + mqtt = hass.components.mqtt + retain = conf[CONF_RETAIN] + + def pub_callback(topic, payload, qos, retain): + """Call MQTT publish function.""" + mqtt.async_publish(topic, payload, qos, retain) + + def sub_callback(topic, sub_cb, qos): + """Call MQTT subscribe function.""" + @callback + def internal_callback(*args): + """Call callback.""" + sub_cb(*args) + + hass.async_add_job( + mqtt.async_subscribe(topic, internal_callback, qos)) + + gateway = mysensors.AsyncMQTTGateway( + pub_callback, sub_callback, in_prefix=in_prefix, + out_prefix=out_prefix, retain=retain, loop=hass.loop, + event_callback=None, persistence=persistence, + persistence_file=persistence_file, + protocol_version=version) + else: + try: + await hass.async_add_job(is_serial_port, device) + gateway = mysensors.AsyncSerialGateway( + device, baud=baud_rate, loop=hass.loop, + event_callback=None, persistence=persistence, + persistence_file=persistence_file, + protocol_version=version) + except vol.Invalid: + try: + await hass.async_add_job(is_socket_address, device) + # valid ip address + gateway = mysensors.AsyncTCPGateway( + device, port=tcp_port, loop=hass.loop, event_callback=None, + persistence=persistence, persistence_file=persistence_file, + protocol_version=version) + except vol.Invalid: + # invalid ip address + return None + gateway.metric = hass.config.units.is_metric + gateway.optimistic = conf[CONF_OPTIMISTIC] + gateway.device = device + gateway.event_callback = _gw_callback_factory(hass) + gateway.nodes_config = gateway_conf[CONF_NODES] + if persistence: + await gateway.start_persistence() + + return gateway + + +async def finish_setup(hass, gateways): + """Load any persistent devices and platforms and start gateway.""" + discover_tasks = [] + start_tasks = [] + for gateway in gateways.values(): + discover_tasks.append(_discover_persistent_devices(hass, gateway)) + start_tasks.append(_gw_start(hass, gateway)) + if discover_tasks: + # Make sure all devices and platforms are loaded before gateway start. + await asyncio.wait(discover_tasks, loop=hass.loop) + if start_tasks: + await asyncio.wait(start_tasks, loop=hass.loop) + + +async def _discover_persistent_devices(hass, gateway): + """Discover platforms for devices loaded via persistence file.""" + tasks = [] + new_devices = defaultdict(list) + for node_id in gateway.sensors: + node = gateway.sensors[node_id] + for child in node.children.values(): + validated = _validate_child(gateway, node_id, child) + for platform, dev_ids in validated.items(): + new_devices[platform].extend(dev_ids) + for platform, dev_ids in new_devices.items(): + tasks.append(_discover_mysensors_platform(hass, platform, dev_ids)) + if tasks: + await asyncio.wait(tasks, loop=hass.loop) + + +@callback +def _discover_mysensors_platform(hass, platform, new_devices): + """Discover a MySensors platform.""" + task = hass.async_add_job(discovery.async_load_platform( + hass, platform, DOMAIN, + {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN})) + return task + + +async def _gw_start(hass, gateway): + """Start the gateway.""" + @callback + def gw_stop(event): + """Trigger to stop the gateway.""" + hass.async_add_job(gateway.stop()) + + 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) + + +def _gw_callback_factory(hass): + """Return a new callback for the gateway.""" + @callback + def mysensors_callback(msg): + """Handle messages from a MySensors gateway.""" + start = timer() + _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: + _LOGGER.debug("Not a child update for node %s", msg.node_id) + return + + signals = [] + + # Update all platforms for the device via dispatcher. + # Add/update entity if schema validates to true. + validated = _validate_child(msg.gateway, msg.node_id, child) + for platform, dev_ids in validated.items(): + devices = get_mysensors_devices(hass, platform) + new_dev_ids = [] + for dev_id in dev_ids: + if dev_id in devices: + signals.append(SIGNAL_CALLBACK.format(*dev_id)) + else: + new_dev_ids.append(dev_id) + if new_dev_ids: + _discover_mysensors_platform(hass, platform, new_dev_ids) + for signal in set(signals): + # Only one signal per device is needed. + # A device can have multiple platforms, ie multiple schemas. + # FOR LATER: Add timer to not signal if another update comes in. + async_dispatcher_send(hass, signal) + end = timer() + if end - start > 0.1: + _LOGGER.debug( + "Callback for node %s child %s took %.3f seconds", + msg.node_id, msg.child_id, end - start) + return mysensors_callback + + +@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): + """Validate that a child has the correct values according to schema. + + Return a dict of platform with a list of device ids for validated devices. + """ + validated = defaultdict(list) + + if not child.values: + _LOGGER.debug( + "No child values for node %s child %s", node_id, child.id) + return validated + if gateway.sensors[node_id].sketch_name is None: + _LOGGER.debug("Node %s is missing sketch name", node_id) + return validated + pres = gateway.const.Presentation + set_req = gateway.const.SetReq + s_name = next( + (member.name for member in pres if member.value == child.type), None) + if s_name not in MYSENSORS_CONST_SCHEMA: + _LOGGER.warning("Child type %s is not supported", s_name) + return validated + child_schemas = MYSENSORS_CONST_SCHEMA[s_name] + + def msg(name): + """Return a message for an invalid schema.""" + return "{} requires value_type {}".format( + pres(child.type).name, set_req[name].name) + + for schema in child_schemas: + platform = schema[PLATFORM] + v_name = schema[TYPE] + value_type = next( + (member.value for member in set_req if member.name == v_name), + None) + if value_type is None: + continue + _child_schema = child.get_schema(gateway.protocol_version) + vol_schema = _child_schema.extend( + {vol.Required(set_req[key].value, msg=msg(key)): + _child_schema.schema.get(set_req[key].value, val) + for key, val in schema.get(SCHEMA, {v_name: cv.string}).items()}, + extra=vol.ALLOW_EXTRA) + try: + vol_schema(child.values) + except vol.Invalid as exc: + level = (logging.WARNING if value_type in child.values + else logging.DEBUG) + _LOGGER.log( + level, + "Invalid values: %s: %s platform: node %s child %s: %s", + child.values, platform, node_id, child.id, exc) + continue + dev_id = id(gateway), node_id, child.id, value_type + validated[platform].append(dev_id) + return validated diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py index db568514dea..71ce7fb0b74 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -18,7 +18,7 @@ async def async_get_service(hass, config, discovery_info=None): return MySensorsNotificationService(hass) -class MySensorsNotificationDevice(mysensors.MySensorsDevice): +class MySensorsNotificationDevice(mysensors.device.MySensorsDevice): """Represent a MySensors Notification device.""" def send_msg(self, msg): diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 1add4157f0e..2fbfc0e97a4 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -42,7 +42,7 @@ async def async_setup_platform( async_add_devices=async_add_devices) -class MySensorsSensor(mysensors.MySensorsEntity): +class MySensorsSensor(mysensors.device.MySensorsEntity): """Representation of a MySensors Sensor child node.""" @property diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index a91ca6d11e7..340eed83b56 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -65,7 +65,7 @@ async def async_setup_platform( schema=SEND_IR_CODE_SERVICE_SCHEMA) -class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice): +class MySensorsSwitch(mysensors.device.MySensorsEntity, SwitchDevice): """Representation of the value of a MySensors Switch child node.""" @property From d3ceb9080c4363db24e7fcff37b771afae483578 Mon Sep 17 00:00:00 2001 From: b3nj1 Date: Mon, 25 Jun 2018 05:04:16 -0700 Subject: [PATCH 062/128] MQTT Alarm Control Panel: add retain option for publishing for cases... (#15134) * MQTT Alarm Control Panel: add retain option for publishing for cases where receiver is asleep * MQTT Alarm Control Panel: add retain option for publishing for cases where receiver is asleep * MQTT Alarm Control Panel: add retain option for publishing for cases where receiver is asleep --- .../components/alarm_control_panel/mqtt.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 8a0dfefdc70..9f2a4176ed8 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, - MqttAvailability) + CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -54,6 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_STATE_TOPIC), config.get(CONF_COMMAND_TOPIC), config.get(CONF_QOS), + config.get(CONF_RETAIN), config.get(CONF_PAYLOAD_DISARM), config.get(CONF_PAYLOAD_ARM_HOME), config.get(CONF_PAYLOAD_ARM_AWAY), @@ -66,9 +67,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): """Representation of a MQTT alarm status.""" - def __init__(self, name, state_topic, command_topic, qos, payload_disarm, - payload_arm_home, payload_arm_away, code, availability_topic, - payload_available, payload_not_available): + def __init__(self, name, state_topic, command_topic, qos, retain, + payload_disarm, payload_arm_home, payload_arm_away, code, + availability_topic, payload_available, payload_not_available): """Init the MQTT Alarm Control Panel.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) @@ -77,6 +78,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): self._state_topic = state_topic self._command_topic = command_topic self._qos = qos + self._retain = retain self._payload_disarm = payload_disarm self._payload_arm_home = payload_arm_home self._payload_arm_away = payload_arm_away @@ -134,7 +136,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): if not self._validate_code(code, 'disarming'): return mqtt.async_publish( - self.hass, self._command_topic, self._payload_disarm, self._qos) + self.hass, self._command_topic, self._payload_disarm, self._qos, + self._retain) @asyncio.coroutine def async_alarm_arm_home(self, code=None): @@ -145,7 +148,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): if not self._validate_code(code, 'arming home'): return mqtt.async_publish( - self.hass, self._command_topic, self._payload_arm_home, self._qos) + self.hass, self._command_topic, self._payload_arm_home, self._qos, + self._retain) @asyncio.coroutine def async_alarm_arm_away(self, code=None): @@ -156,7 +160,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): if not self._validate_code(code, 'arming away'): return mqtt.async_publish( - self.hass, self._command_topic, self._payload_arm_away, self._qos) + self.hass, self._command_topic, self._payload_arm_away, self._qos, + self._retain) def _validate_code(self, code, state): """Validate given code.""" From 73034c933ed6cc856a7d656c3c41e474fd7d33e0 Mon Sep 17 00:00:00 2001 From: dreizehnelf Date: Mon, 25 Jun 2018 15:13:19 +0200 Subject: [PATCH 063/128] Add discovery support to mqtt climate component. (#15085) * Add discovery support to mqtt climate component. * - Fix flake8 error (./homeassistant/components/climate/mqtt.py:130:1: D202 No blank lines allowed after function docstring) - Fix test error (since climate component was expected not to work - changed it to "lock" component, which also does not have MQTT discovery support yet) * Fix old assert statement to reflect new lock component usage * Change invalid MQTT discovery component type from 'lock' to 'timer', since contrary to the documentation the lock component is properly supported when using MQTT discovery. * Make configuration of invalid MQTT config component a single point of entry to prevent missing the assertion later in the code when changing. * Add new testcases to cover not-yet-covered code paths in https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/climate/mqtt.py --- homeassistant/components/climate/mqtt.py | 3 + homeassistant/components/mqtt/discovery.py | 3 +- tests/components/climate/test_mqtt.py | 82 +++++++++++++++++++++- tests/components/mqtt/test_discovery.py | 34 ++++++++- 4 files changed, 118 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 5397daeb784..2878717d91b 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -129,6 +129,9 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the MQTT climate devices.""" + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) + template_keys = ( CONF_POWER_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE, diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d5a3b4a2efb..3916714b8d1 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -21,7 +21,7 @@ TOPIC_MATCHER = re.compile( SUPPORTED_COMPONENTS = [ 'binary_sensor', 'camera', 'cover', 'fan', - 'light', 'sensor', 'switch', 'lock'] + 'light', 'sensor', 'switch', 'lock', 'climate'] ALLOWED_PLATFORMS = { 'binary_sensor': ['mqtt'], @@ -32,6 +32,7 @@ ALLOWED_PLATFORMS = { 'lock': ['mqtt'], 'sensor': ['mqtt'], 'switch': ['mqtt'], + 'climate': ['mqtt'], } ALREADY_DISCOVERED = 'mqtt_discovered_components' diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 255d482d584..5db77331cd4 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -137,6 +137,37 @@ class TestMQTTClimate(unittest.TestCase): self.assertEqual("cool", state.attributes.get('operation_mode')) self.assertEqual("cool", state.state) + def test_set_operation_with_power_command(self): + """Test setting of new operation mode with power command enabled.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['power_command_topic'] = 'power-command' + assert setup_component(self.hass, climate.DOMAIN, config) + + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("off", state.attributes.get('operation_mode')) + self.assertEqual("off", state.state) + climate.set_operation_mode(self.hass, "on", ENTITY_CLIMATE) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("on", state.attributes.get('operation_mode')) + self.assertEqual("on", state.state) + self.mock_publish.async_publish.assert_has_calls([ + unittest.mock.call('power-command', 'ON', 0, False), + unittest.mock.call('mode-topic', 'on', 0, False) + ]) + self.mock_publish.async_publish.reset_mock() + + climate.set_operation_mode(self.hass, "off", ENTITY_CLIMATE) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual("off", state.attributes.get('operation_mode')) + self.assertEqual("off", state.state) + self.mock_publish.async_publish.assert_has_calls([ + unittest.mock.call('power-command', 'OFF', 0, False), + unittest.mock.call('mode-topic', 'off', 0, False) + ]) + self.mock_publish.async_publish.reset_mock() + def test_set_fan_mode_bad_attr(self): """Test setting fan mode without required attribute.""" assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) @@ -241,6 +272,8 @@ class TestMQTTClimate(unittest.TestCase): self.assertEqual(21, state.attributes.get('temperature')) climate.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('heat', state.attributes.get('operation_mode')) self.mock_publish.async_publish.assert_called_once_with( 'mode-topic', 'heat', 0, False) self.mock_publish.async_publish.reset_mock() @@ -252,6 +285,21 @@ class TestMQTTClimate(unittest.TestCase): self.mock_publish.async_publish.assert_called_once_with( 'temperature-topic', 47, 0, False) + # also test directly supplying the operation mode to set_temperature + self.mock_publish.async_publish.reset_mock() + climate.set_temperature(self.hass, temperature=21, + operation_mode="cool", + entity_id=ENTITY_CLIMATE) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('cool', state.attributes.get('operation_mode')) + self.assertEqual(21, state.attributes.get('temperature')) + self.mock_publish.async_publish.assert_has_calls([ + unittest.mock.call('mode-topic', 'cool', 0, False), + unittest.mock.call('temperature-topic', 21, 0, False) + ]) + self.mock_publish.async_publish.reset_mock() + def test_set_target_temperature_pessimistic(self): """Test setting the target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -508,13 +556,28 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("on", state.attributes.get('swing_mode')) - # Temperature + # Temperature - with valid value self.assertEqual(21, state.attributes.get('temperature')) fire_mqtt_message(self.hass, 'temperature-state', '"1031"') self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(1031, state.attributes.get('temperature')) + # Temperature - with invalid value + with self.assertLogs(level='ERROR') as log: + fire_mqtt_message(self.hass, 'temperature-state', '"-INVALID-"') + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + # make sure, the invalid value gets logged... + self.assertEqual(len(log.output), 1) + self.assertEqual(len(log.records), 1) + self.assertIn( + "Could not parse temperature from -INVALID-", + log.output[0] + ) + # ... but the actual value stays unchanged. + self.assertEqual(1031, state.attributes.get('temperature')) + # Away Mode self.assertEqual('off', state.attributes.get('away_mode')) fire_mqtt_message(self.hass, 'away-state', '"ON"') @@ -522,6 +585,17 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('away_mode')) + # Away Mode with JSON values + fire_mqtt_message(self.hass, 'away-state', 'false') + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('off', state.attributes.get('away_mode')) + + fire_mqtt_message(self.hass, 'away-state', 'true') + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('on', state.attributes.get('away_mode')) + # Hold Mode self.assertEqual(None, state.attributes.get('hold_mode')) fire_mqtt_message(self.hass, 'hold-state', """ @@ -538,6 +612,12 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('aux_heat')) + # anything other than 'switchmeon' should turn Aux mode off + fire_mqtt_message(self.hass, 'aux-state', 'somerandomstring') + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_CLIMATE) + self.assertEqual('off', state.attributes.get('aux_heat')) + # Current temperature fire_mqtt_message(self.hass, 'current-temperature', '"74656"') self.hass.block_till_done() diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 1dd29909ffd..ed6c77f676c 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -52,12 +52,21 @@ def test_invalid_json(mock_load_platform, hass, mqtt_mock, caplog): @asyncio.coroutine def test_only_valid_components(mock_load_platform, hass, mqtt_mock, caplog): """Test for a valid component.""" + invalid_component = "timer" + mock_load_platform.return_value = mock_coro() yield from async_start(hass, 'homeassistant', {}) - async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', '{}') + async_fire_mqtt_message(hass, 'homeassistant/{}/bla/config'.format( + invalid_component + ), '{}') + yield from hass.async_block_till_done() - assert 'Component climate is not supported' in caplog.text + + assert 'Component {} is not supported'.format( + invalid_component + ) in caplog.text + assert not mock_load_platform.called @@ -94,6 +103,27 @@ def test_discover_fan(hass, mqtt_mock, caplog): assert ('fan', 'bla') in hass.data[ALREADY_DISCOVERED] +@asyncio.coroutine +def test_discover_climate(hass, mqtt_mock, caplog): + """Test discovering an MQTT climate component.""" + yield from async_start(hass, 'homeassistant', {}) + + data = ( + '{ "name": "ClimateTest",' + ' "current_temperature_topic": "climate/bla/current_temp",' + ' "temperature_command_topic": "climate/bla/target_temp" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', data) + yield from hass.async_block_till_done() + + state = hass.states.get('climate.ClimateTest') + + assert state is not None + assert state.name == 'ClimateTest' + assert ('climate', 'bla') in hass.data[ALREADY_DISCOVERED] + + @asyncio.coroutine def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): """Test sending in correct JSON with optional node_id included.""" From 038168c417a5e43f4cb0dfd868a84616e71c2e22 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Mon, 25 Jun 2018 09:45:26 -0400 Subject: [PATCH 064/128] Support for Homekit Controller climate devices (#15057) * Support for Homekit Controller climate devices * Handle stale state when operating mode off --- .../components/climate/homekit_controller.py | 130 ++++++++++++++++++ .../components/homekit_controller/__init__.py | 6 + .../components/light/homekit_controller.py | 7 +- .../components/switch/homekit_controller.py | 7 +- 4 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/climate/homekit_controller.py diff --git a/homeassistant/components/climate/homekit_controller.py b/homeassistant/components/climate/homekit_controller.py new file mode 100644 index 00000000000..f9178c2e0d5 --- /dev/null +++ b/homeassistant/components/climate/homekit_controller.py @@ -0,0 +1,130 @@ +""" +Support for Homekit climate devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.homekit_controller/ +""" +import logging + +from homeassistant.components.homekit_controller import ( + HomeKitEntity, KNOWN_ACCESSORIES) +from homeassistant.components.climate import ( + ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) +from homeassistant.const import TEMP_CELSIUS, STATE_OFF, ATTR_TEMPERATURE + +DEPENDENCIES = ['homekit_controller'] + +_LOGGER = logging.getLogger(__name__) + +# Map of Homekit operation modes to hass modes +MODE_HOMEKIT_TO_HASS = { + 0: STATE_OFF, + 1: STATE_HEAT, + 2: STATE_COOL, +} + +# Map of hass operation modes to homekit modes +MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Homekit climate.""" + if discovery_info is not None: + accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']] + add_devices([HomeKitClimateDevice(accessory, discovery_info)], True) + + +class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): + """Representation of a Homekit climate device.""" + + def __init__(self, *args): + """Initialise the device.""" + super().__init__(*args) + self._state = None + self._current_mode = None + self._valid_modes = [] + self._current_temp = None + self._target_temp = None + + def update_characteristics(self, characteristics): + """Synchronise device state with Home Assistant.""" + # pylint: disable=import-error + from homekit import CharacteristicsTypes as ctypes + + for characteristic in characteristics: + ctype = characteristic['type'] + if ctype == ctypes.HEATING_COOLING_CURRENT: + self._state = MODE_HOMEKIT_TO_HASS.get( + characteristic['value']) + if ctype == ctypes.HEATING_COOLING_TARGET: + self._chars['target_mode'] = characteristic['iid'] + self._features |= SUPPORT_OPERATION_MODE + self._current_mode = MODE_HOMEKIT_TO_HASS.get( + characteristic['value']) + self._valid_modes = [MODE_HOMEKIT_TO_HASS.get( + mode) for mode in characteristic['valid-values']] + elif ctype == ctypes.TEMPERATURE_CURRENT: + self._current_temp = characteristic['value'] + elif ctype == ctypes.TEMPERATURE_TARGET: + self._chars['target_temp'] = characteristic['iid'] + self._features |= SUPPORT_TARGET_TEMPERATURE + self._target_temp = characteristic['value'] + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + + characteristics = [{'aid': self._aid, + 'iid': self._chars['target_temp'], + 'value': temp}] + self.put_characteristics(characteristics) + + def set_operation_mode(self, operation_mode): + """Set new target operation mode.""" + characteristics = [{'aid': self._aid, + 'iid': self._chars['target_mode'], + 'value': MODE_HASS_TO_HOMEKIT[operation_mode]}] + self.put_characteristics(characteristics) + + @property + def state(self): + """Return the current state.""" + # If the device reports its operating mode as off, it sometimes doesn't + # report a new state. + if self._current_mode == STATE_OFF: + return STATE_OFF + + if self._state == STATE_OFF and self._current_mode != STATE_OFF: + return STATE_IDLE + return self._state + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temp + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temp + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._current_mode + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return self._valid_modes + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._features + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 0883c5a3cc8..ff981c1607a 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -23,6 +23,7 @@ HOMEKIT_DIR = '.homekit' HOMEKIT_ACCESSORY_DISPATCH = { 'lightbulb': 'light', 'outlet': 'switch', + 'thermostat': 'climate', } KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN) @@ -219,6 +220,11 @@ class HomeKitEntity(Entity): """Synchronise a HomeKit device state with Home Assistant.""" raise NotImplementedError + def put_characteristics(self, characteristics): + """Control a HomeKit device state from Home Assistant.""" + body = json.dumps({'characteristics': characteristics}) + self._securecon.put('/characteristics', body) + # pylint: too-many-function-args def setup(hass, config): diff --git a/homeassistant/components/light/homekit_controller.py b/homeassistant/components/light/homekit_controller.py index e6dc09e455c..8d77cb05236 100644 --- a/homeassistant/components/light/homekit_controller.py +++ b/homeassistant/components/light/homekit_controller.py @@ -4,7 +4,6 @@ Support for Homekit lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.homekit_controller/ """ -import json import logging from homeassistant.components.homekit_controller import ( @@ -122,13 +121,11 @@ class HomeKitLight(HomeKitEntity, Light): characteristics.append({'aid': self._aid, 'iid': self._chars['on'], 'value': True}) - body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self.put_characteristics(characteristics) def turn_off(self, **kwargs): """Turn the specified light off.""" characteristics = [{'aid': self._aid, 'iid': self._chars['on'], 'value': False}] - body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self.put_characteristics(characteristics) diff --git a/homeassistant/components/switch/homekit_controller.py b/homeassistant/components/switch/homekit_controller.py index 6b97200ba49..3293c8fe195 100644 --- a/homeassistant/components/switch/homekit_controller.py +++ b/homeassistant/components/switch/homekit_controller.py @@ -4,7 +4,6 @@ Support for Homekit switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.homekit_controller/ """ -import json import logging from homeassistant.components.homekit_controller import (HomeKitEntity, @@ -56,13 +55,11 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice): characteristics = [{'aid': self._aid, 'iid': self._chars['on'], 'value': True}] - body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self.put_characteristics(characteristics) def turn_off(self, **kwargs): """Turn the specified switch off.""" characteristics = [{'aid': self._aid, 'iid': self._chars['on'], 'value': False}] - body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self.put_characteristics(characteristics) From f8bc3411adac137b134aef6e60f57b0f490783c9 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 25 Jun 2018 15:57:26 +0200 Subject: [PATCH 065/128] PyPi: Fix description and setup.cfg (#15107) * Fix description and extend use of setup.cfg * Fix lint --- setup.cfg | 31 +++++++++++++++++++++++++++++++ setup.py | 44 ++++++++++---------------------------------- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8b17da455dc..2abd445bb85 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,34 @@ +[metadata] +license = Apache License 2.0 +license_file = LICENSE.md +platforms = any +description = Open-source home automation platform running on Python 3. +long_description = file: README.rst +keywords = home, automation +classifier = + Development Status :: 4 - Beta + Intended Audience :: End Users/Desktop + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Operating System :: OS Independent + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Topic :: Home Automation + +[options] +packages = find: +include_package_data = true +zip_safe = false + +[options.entry_points] +console_scripts = + hass = homeassistant.__main__:main + +[options.packages.find] +exclude = + tests + tests.* + [tool:pytest] testpaths = tests norecursedirs = .git testing_config diff --git a/setup.py b/setup.py index 69929285f78..3833f90f2d1 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """Home Assistant setup script.""" -from setuptools import setup, find_packages +from datetime import datetime as dt +from setuptools import setup import homeassistant.const as hass_const @@ -8,26 +9,9 @@ PROJECT_NAME = 'Home Assistant' PROJECT_PACKAGE_NAME = 'homeassistant' PROJECT_LICENSE = 'Apache License 2.0' PROJECT_AUTHOR = 'The Home Assistant Authors' -PROJECT_COPYRIGHT = ' 2013-2018, {}'.format(PROJECT_AUTHOR) +PROJECT_COPYRIGHT = ' 2013-{}, {}'.format(dt.now().year, PROJECT_AUTHOR) PROJECT_URL = 'https://home-assistant.io/' PROJECT_EMAIL = 'hello@home-assistant.io' -PROJECT_DESCRIPTION = ('Open-source home automation platform ' - 'running on Python 3.') -PROJECT_LONG_DESCRIPTION = ('Home Assistant is an open-source ' - 'home automation platform running on Python 3. ' - 'Track and control all devices at home and ' - 'automate control. ' - 'Installation in less than a minute.') -PROJECT_CLASSIFIERS = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: End Users/Desktop', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Home Automation' -] PROJECT_GITHUB_USERNAME = 'home-assistant' PROJECT_GITHUB_REPOSITORY = 'home-assistant' @@ -38,8 +22,12 @@ GITHUB_PATH = '{}/{}'.format( GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) - -PACKAGES = find_packages(exclude=['tests', 'tests.*']) +PROJECT_URLS = { + 'Bug Reports': '{}/issues'.format(GITHUB_URL), + 'Dev Docs': 'https://developers.home-assistant.io/', + 'Discord': 'https://discordapp.com/invite/c5DvZ4e', + 'Forum': 'https://community.home-assistant.io/', +} REQUIRES = [ 'aiohttp==3.3.2', @@ -60,24 +48,12 @@ MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) setup( name=PROJECT_PACKAGE_NAME, version=hass_const.__version__, - license=PROJECT_LICENSE, url=PROJECT_URL, download_url=DOWNLOAD_URL, + project_urls=PROJECT_URLS, author=PROJECT_AUTHOR, author_email=PROJECT_EMAIL, - description=PROJECT_DESCRIPTION, - packages=PACKAGES, - include_package_data=True, - zip_safe=False, - platforms='any', install_requires=REQUIRES, python_requires='>={}'.format(MIN_PY_VERSION), test_suite='tests', - keywords=['home', 'automation'], - entry_points={ - 'console_scripts': [ - 'hass = homeassistant.__main__:main' - ] - }, - classifiers=PROJECT_CLASSIFIERS, ) From 672a3c7178afc91ac0da015f098dd5f96672e4f2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 25 Jun 2018 16:35:44 +0200 Subject: [PATCH 066/128] Add language to dark sky weather component (#15130) * Add language to dark sky weather component * Update darksky.py --- homeassistant/components/weather/darksky.py | 28 ++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index f0712542ea5..86cc740edbc 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -25,9 +25,22 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by Dark Sky" +# Language Supported Codes +LANGUAGE_CODES = [ + 'ar', 'az', 'be', 'bg', 'bs', 'ca', + 'cs', 'da', 'de', 'el', 'en', 'es', + 'et', 'fi', 'fr', 'hr', 'hu', 'id', + 'is', 'it', 'ja', 'ka', 'kw', 'nb', + 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', + 'sl', 'sr', 'sv', 'tet', 'tr', 'uk', + 'x-pig-latin', 'zh', 'zh-tw', +] + CONF_UNITS = 'units' +CONF_LANGUAGE = 'language' DEFAULT_NAME = 'Dark Sky' +DEFAULT_LANGUAGE = 'en' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, @@ -35,6 +48,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_LONGITUDE): cv.longitude, vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_LANGUAGE, + default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), }) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=3) @@ -44,15 +59,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Dark Sky weather.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - name = config.get(CONF_NAME) + name = config[CONF_NAME] + lang = config[CONF_LANGUAGE] + api_key = config[CONF_API_KEY] units = config.get(CONF_UNITS) if not units: units = 'si' if hass.config.units.is_metric else 'us' - dark_sky = DarkSkyData( - config.get(CONF_API_KEY), latitude, longitude, units) - + dark_sky = DarkSkyData(api_key, latitude, longitude, units, lang) add_devices([DarkSkyWeather(name, dark_sky)], True) @@ -132,12 +147,13 @@ class DarkSkyWeather(WeatherEntity): class DarkSkyData(object): """Get the latest data from Dark Sky.""" - def __init__(self, api_key, latitude, longitude, units): + def __init__(self, api_key, latitude, longitude, units, lang): """Initialize the data object.""" self._api_key = api_key self.latitude = latitude self.longitude = longitude self.requested_units = units + self.language = lang self.data = None self.currently = None @@ -152,7 +168,7 @@ class DarkSkyData(object): try: self.data = forecastio.load_forecast( self._api_key, self.latitude, self.longitude, - units=self.requested_units) + units=self.requested_units, lang=self.language) self.currently = self.data.currently() self.hourly = self.data.hourly() self.daily = self.data.daily() From ae51dc08bf5cb947d14b0e55fb64387b31924ac5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Jun 2018 12:53:49 -0400 Subject: [PATCH 067/128] Add storage helper and migrate config entries (#15045) * Add storage helper * Migrate config entries to use the storage helper * Make sure tests do not do I/O * Lint * Add versions to stored data * Add more instance variables * Make migrator load config if nothing to migrate * Address comments --- homeassistant/components/sensor/fitbit.py | 2 +- homeassistant/config_entries.py | 60 ++++---- homeassistant/core.py | 14 ++ homeassistant/helpers/storage.py | 157 +++++++++++++++++++++ homeassistant/util/json.py | 14 +- tests/common.py | 8 +- tests/helpers/test_storage.py | 158 ++++++++++++++++++++++ tests/test_config_entries.py | 17 ++- 8 files changed, 384 insertions(+), 46 deletions(-) create mode 100644 homeassistant/helpers/storage.py create mode 100644 tests/helpers/test_storage.py diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index f312d1f22cc..87bd735a03d 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -225,7 +225,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, config, add_devices, config_path, discovery_info=None) return False else: - config_file = save_json(config_path, DEFAULT_CONFIG) + save_json(config_path, DEFAULT_CONFIG) request_app_setup( hass, config, add_devices, config_path, discovery_info=None) return False diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index db2912d7b42..13cb7de62ef 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -112,15 +112,13 @@ the flow from the config panel. """ import logging -import os import uuid -from . import data_entry_flow -from .core import callback -from .exceptions import HomeAssistantError -from .setup import async_setup_component, async_process_deps_reqs -from .util.json import load_json, save_json -from .util.decorator import Registry +from homeassistant import data_entry_flow +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component, async_process_deps_reqs +from homeassistant.util.decorator import Registry _LOGGER = logging.getLogger(__name__) @@ -136,6 +134,10 @@ FLOWS = [ ] +STORAGE_KEY = 'core.config_entries' +STORAGE_VERSION = 1 + +# Deprecated since 0.73 PATH_CONFIG = '.config_entries.json' SAVE_DELAY = 1 @@ -271,7 +273,7 @@ class ConfigEntries: hass, self._async_create_flow, self._async_finish_flow) self._hass_config = hass_config self._entries = None - self._sched_save = None + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback def async_domains(self): @@ -305,7 +307,7 @@ class ConfigEntries: raise UnknownEntry entry = self._entries.pop(found) - self._async_schedule_save() + await self._async_schedule_save() unloaded = await entry.async_unload(self.hass) @@ -314,14 +316,14 @@ class ConfigEntries: } async def async_load(self): - """Load the config.""" - path = self.hass.config.path(PATH_CONFIG) - if not os.path.isfile(path): - self._entries = [] - return + """Handle loading the config.""" + # Migrating for config entries stored before 0.73 + config = await self.hass.helpers.storage.async_migrator( + self.hass.config.path(PATH_CONFIG), self._store, + old_conf_migrate_func=_old_conf_migrator + ) - entries = await self.hass.async_add_job(load_json, path) - self._entries = [ConfigEntry(**entry) for entry in entries] + self._entries = [ConfigEntry(**entry) for entry in config['entries']] async def async_forward_entry_setup(self, entry, component): """Forward the setup of an entry to a different component. @@ -372,7 +374,7 @@ class ConfigEntries: source=result['source'], ) self._entries.append(entry) - self._async_schedule_save() + await self._async_schedule_save() # Setup entry if entry.domain in self.hass.config.components: @@ -416,20 +418,14 @@ class ConfigEntries: return handler() - @callback - def _async_schedule_save(self): - """Schedule saving the entity registry.""" - if self._sched_save is not None: - self._sched_save.cancel() - - self._sched_save = self.hass.loop.call_later( - SAVE_DELAY, self.hass.async_add_job, self._async_save - ) - - async def _async_save(self): + async def _async_schedule_save(self): """Save the entity registry to a file.""" - self._sched_save = None - data = [entry.as_dict() for entry in self._entries] + data = { + 'entries': [entry.as_dict() for entry in self._entries] + } + await self._store.async_save(data, delay=SAVE_DELAY) - await self.hass.async_add_job( - save_json, self.hass.config.path(PATH_CONFIG), data) + +async def _old_conf_migrator(old_config): + """Migrate the pre-0.73 config format to the latest version.""" + return {'entries': old_config} diff --git a/homeassistant/core.py b/homeassistant/core.py index 5e6dcd81310..e0950172913 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -230,6 +230,20 @@ class HomeAssistant(object): return task + @callback + def async_add_executor_job( + self, + target: Callable[..., Any], + *args: Any) -> asyncio.tasks.Task: + """Add an executor job from within the event loop.""" + task = self.loop.run_in_executor(None, target, *args) + + # If a task is scheduled + if self._track_task: + self._pending_tasks.append(task) + + return task + @callback def async_track_tasks(self): """Track tasks so you can wait for all tasks to be done.""" diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py new file mode 100644 index 00000000000..4b0c576f129 --- /dev/null +++ b/homeassistant/helpers/storage.py @@ -0,0 +1,157 @@ +"""Helper to help store data.""" +import asyncio +import logging +import os +from typing import Dict, Optional + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback +from homeassistant.loader import bind_hass +from homeassistant.util import json +from homeassistant.helpers.event import async_call_later + +STORAGE_DIR = '.storage' +_LOGGER = logging.getLogger(__name__) + + +@bind_hass +async def async_migrator(hass, old_path, store, *, old_conf_migrate_func=None): + """Helper function to migrate old data to a store and then load data. + + async def old_conf_migrate_func(old_data) + """ + def load_old_config(): + """Helper to load old config.""" + if not os.path.isfile(old_path): + return None + + return json.load_json(old_path) + + config = await hass.async_add_executor_job(load_old_config) + + if config is None: + return await store.async_load() + + if old_conf_migrate_func is not None: + config = await old_conf_migrate_func(config) + + await store.async_save(config) + await hass.async_add_executor_job(os.remove, old_path) + return config + + +@bind_hass +class Store: + """Class to help storing data.""" + + def __init__(self, hass, version: int, key: str): + """Initialize storage class.""" + self.version = version + self.key = key + self.hass = hass + self._data = None + self._unsub_delay_listener = None + self._unsub_stop_listener = None + self._write_lock = asyncio.Lock() + + @property + def path(self): + """Return the config path.""" + return self.hass.config.path(STORAGE_DIR, self.key) + + async def async_load(self): + """Load data. + + If the expected version does not match the given version, the migrate + function will be invoked with await migrate_func(version, config). + """ + if self._data is not None: + data = self._data + else: + data = await self.hass.async_add_executor_job( + json.load_json, self.path, None) + + if data is None: + return {} + + if data['version'] == self.version: + return data['data'] + + return await self._async_migrate_func(data['version'], data['data']) + + async def async_save(self, data: Dict, *, delay: Optional[int] = None): + """Save data with an optional delay.""" + self._data = { + 'version': self.version, + 'key': self.key, + 'data': data, + } + + self._async_cleanup_delay_listener() + + if delay is None: + self._async_cleanup_stop_listener() + await self._async_handle_write_data() + return + + self._unsub_delay_listener = async_call_later( + self.hass, delay, self._async_callback_delayed_write) + + self._async_ensure_stop_listener() + + @callback + def _async_ensure_stop_listener(self): + """Ensure that we write if we quit before delay has passed.""" + if self._unsub_stop_listener is None: + self._unsub_stop_listener = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_callback_stop_write) + + @callback + def _async_cleanup_stop_listener(self): + """Clean up a stop listener.""" + if self._unsub_stop_listener is not None: + self._unsub_stop_listener() + self._unsub_stop_listener = None + + @callback + def _async_cleanup_delay_listener(self): + """Clean up a delay listener.""" + if self._unsub_delay_listener is not None: + self._unsub_delay_listener() + self._unsub_delay_listener = None + + async def _async_callback_delayed_write(self, _now): + """Handle a delayed write callback.""" + self._unsub_delay_listener = None + self._async_cleanup_stop_listener() + await self._async_handle_write_data() + + async def _async_callback_stop_write(self, _event): + """Handle a write because Home Assistant is stopping.""" + self._unsub_stop_listener = None + self._async_cleanup_delay_listener() + await self._async_handle_write_data() + + async def _async_handle_write_data(self, *_args): + """Handler to handle writing the config.""" + data = self._data + self._data = None + + async with self._write_lock: + try: + await self.hass.async_add_executor_job( + self._write_data, self.path, data) + except (json.SerializationError, json.WriteError) as err: + _LOGGER.error('Error writing config for %s: %s', self.key, err) + + def _write_data(self, path: str, data: Dict): + """Write the data.""" + if not os.path.isdir(os.path.dirname(path)): + os.makedirs(os.path.dirname(path)) + + _LOGGER.debug('Writing data for %s', self.key) + json.save_json(path, data) + + async def _async_migrate_func(self, old_version, old_data): + """Migrate to the new version.""" + raise NotImplementedError diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index b2577ff6be6..0e53342b0ca 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -11,6 +11,14 @@ _LOGGER = logging.getLogger(__name__) _UNDEFINED = object() +class SerializationError(HomeAssistantError): + """Error serializing the data to JSON.""" + + +class WriteError(HomeAssistantError): + """Error writing the data.""" + + def load_json(filename: str, default: Union[List, Dict] = _UNDEFINED) \ -> Union[List, Dict]: """Load JSON data from a file and return as dict or list. @@ -41,13 +49,11 @@ def save_json(filename: str, data: Union[List, Dict]): data = json.dumps(data, sort_keys=True, indent=4) with open(filename, 'w', encoding='utf-8') as fdesc: fdesc.write(data) - return True except TypeError as error: _LOGGER.exception('Failed to serialize to JSON: %s', filename) - raise HomeAssistantError(error) + raise SerializationError(error) except OSError as error: _LOGGER.exception('Saving JSON file failed: %s', filename) - raise HomeAssistantError(error) - return False + raise WriteError(error) diff --git a/tests/common.py b/tests/common.py index 556935a6ac1..56575bdb1e9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -14,7 +14,7 @@ from homeassistant import auth, core as ha, data_entry_flow, config_entries from homeassistant.setup import setup_component, async_setup_component from homeassistant.config import async_process_component_config from homeassistant.helpers import ( - intent, entity, restore_state, entity_registry, + intent, entity, restore_state, entity_registry, entity_platform) from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.dt as date_util @@ -110,8 +110,6 @@ def get_test_home_assistant(): def async_test_home_assistant(loop): """Return a Home Assistant object pointing at test config dir.""" hass = ha.HomeAssistant(loop) - hass.config_entries = config_entries.ConfigEntries(hass, {}) - hass.config_entries._entries = [] hass.config.async_load = Mock() store = auth.AuthStore(hass) hass.auth = auth.AuthManager(hass, store, {}) @@ -137,6 +135,10 @@ def async_test_home_assistant(loop): hass.config.units = METRIC_SYSTEM hass.config.skip_pip = True + hass.config_entries = config_entries.ConfigEntries(hass, {}) + hass.config_entries._entries = [] + hass.config_entries._store._async_ensure_stop_listener = lambda: None + hass.state = ha.CoreState.running # Mock async_start diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py new file mode 100644 index 00000000000..289d07edab2 --- /dev/null +++ b/tests/helpers/test_storage.py @@ -0,0 +1,158 @@ +"""Tests for the storage helper.""" +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import storage +from homeassistant.util import dt + +from tests.common import async_fire_time_changed, mock_coro + + +MOCK_VERSION = 1 +MOCK_KEY = 'storage-test' +MOCK_DATA = {'hello': 'world'} + + +@pytest.fixture +def mock_save(): + """Fixture to mock JSON save.""" + written = [] + with patch('homeassistant.util.json.save_json', + side_effect=lambda *args: written.append(args)): + yield written + + +@pytest.fixture +def mock_load(mock_save): + """Fixture to mock JSON read.""" + with patch('homeassistant.util.json.load_json', + side_effect=lambda *args: mock_save[-1][1]): + yield + + +@pytest.fixture +def store(hass): + """Fixture of a store that prevents writing on HASS stop.""" + store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) + store._async_ensure_stop_listener = lambda: None + yield store + + +async def test_loading(hass, store, mock_save, mock_load): + """Test we can save and load data.""" + await store.async_save(MOCK_DATA) + data = await store.async_load() + assert data == MOCK_DATA + + +async def test_loading_non_existing(hass, store): + """Test we can save and load data.""" + with patch('homeassistant.util.json.open', side_effect=FileNotFoundError): + data = await store.async_load() + assert data == {} + + +async def test_saving_with_delay(hass, store, mock_save): + """Test saving data after a delay.""" + await store.async_save(MOCK_DATA, delay=1) + assert len(mock_save) == 0 + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + assert len(mock_save) == 1 + + +async def test_saving_on_stop(hass, mock_save): + """Test delayed saves trigger when we quit Home Assistant.""" + store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) + await store.async_save(MOCK_DATA, delay=1) + assert len(mock_save) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert len(mock_save) == 1 + + +async def test_loading_while_delay(hass, store, mock_save, mock_load): + """Test we load new data even if not written yet.""" + await store.async_save({'delay': 'no'}) + assert len(mock_save) == 1 + + await store.async_save({'delay': 'yes'}, delay=1) + assert len(mock_save) == 1 + + data = await store.async_load() + assert data == {'delay': 'yes'} + + +async def test_writing_while_writing_delay(hass, store, mock_save, mock_load): + """Test a write while a write with delay is active.""" + await store.async_save({'delay': 'yes'}, delay=1) + assert len(mock_save) == 0 + await store.async_save({'delay': 'no'}) + assert len(mock_save) == 1 + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + await hass.async_block_till_done() + assert len(mock_save) == 1 + + data = await store.async_load() + assert data == {'delay': 'no'} + + +async def test_migrator_no_existing_config(hass, store, mock_save): + """Test migrator with no existing config.""" + with patch('os.path.isfile', return_value=False), \ + patch.object(store, 'async_load', + return_value=mock_coro({'cur': 'config'})): + data = await storage.async_migrator( + hass, 'old-path', store) + + assert data == {'cur': 'config'} + assert len(mock_save) == 0 + + +async def test_migrator_existing_config(hass, store, mock_save): + """Test migrating existing config.""" + with patch('os.path.isfile', return_value=True), \ + patch('os.remove') as mock_remove, \ + patch('homeassistant.util.json.load_json', + return_value={'old': 'config'}): + data = await storage.async_migrator( + hass, 'old-path', store) + + assert len(mock_remove.mock_calls) == 1 + assert data == {'old': 'config'} + assert len(mock_save) == 1 + assert mock_save[0][1] == { + 'key': MOCK_KEY, + 'version': MOCK_VERSION, + 'data': data, + } + + +async def test_migrator_transforming_config(hass, store, mock_save): + """Test migrating config to new format.""" + async def old_conf_migrate_func(old_config): + """Migrate old config to new format.""" + return {'new': old_config['old']} + + with patch('os.path.isfile', return_value=True), \ + patch('os.remove') as mock_remove, \ + patch('homeassistant.util.json.load_json', + return_value={'old': 'config'}): + data = await storage.async_migrator( + hass, 'old-path', store, + old_conf_migrate_func=old_conf_migrate_func) + + assert len(mock_remove.mock_calls) == 1 + assert data == {'new': 'config'} + assert len(mock_save) == 1 + assert mock_save[0][1] == { + 'key': MOCK_KEY, + 'version': MOCK_VERSION, + 'data': data, + } diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 84bd0771542..fc0a549f1ae 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,13 +1,16 @@ """Test the config manager.""" import asyncio +from datetime import timedelta from unittest.mock import MagicMock, patch, mock_open import pytest from homeassistant import config_entries, loader, data_entry_flow from homeassistant.setup import async_setup_component +from homeassistant.util import dt -from tests.common import MockModule, mock_coro, MockConfigEntry +from tests.common import ( + MockModule, mock_coro, MockConfigEntry, async_fire_time_changed) @pytest.fixture @@ -15,6 +18,7 @@ def manager(hass): """Fixture of a loaded config manager.""" manager = config_entries.ConfigEntries(hass, {}) manager._entries = [] + manager._store._async_ensure_stop_listener = lambda: None hass.config_entries = manager return manager @@ -151,7 +155,9 @@ def test_domains_gets_uniques(manager): @asyncio.coroutine def test_saving_and_loading(hass): """Test that we're saving and loading correctly.""" - loader.set_component(hass, 'test', MockModule('test')) + loader.set_component( + hass, 'test', + MockModule('test', async_setup_entry=lambda *args: mock_coro(True))) class TestFlow(data_entry_flow.FlowHandler): VERSION = 5 @@ -183,13 +189,12 @@ def test_saving_and_loading(hass): json_path = 'homeassistant.util.json.open' with patch('homeassistant.config_entries.HANDLERS.get', - return_value=Test2Flow), \ - patch.object(config_entries, 'SAVE_DELAY', 0): + return_value=Test2Flow): yield from hass.config_entries.flow.async_init('test') with patch(json_path, mock_open(), create=True) as mock_write: # To trigger the call_later - yield from asyncio.sleep(0, loop=hass.loop) + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) # To execute the save yield from hass.async_block_till_done() @@ -199,7 +204,7 @@ def test_saving_and_loading(hass): # Now load written data in new config manager manager = config_entries.ConfigEntries(hass, {}) - with patch('os.path.isfile', return_value=True), \ + with patch('os.path.isfile', return_value=False), \ patch(json_path, mock_open(read_data=written), create=True): yield from manager.async_load() From dbae410cf40f3a684ee5500b0691bed4c5c65204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 25 Jun 2018 19:55:03 +0300 Subject: [PATCH 068/128] Fix pylintrc section order and option placements (#15120) --- pylintrc | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pylintrc b/pylintrc index df839b379b5..d47437cb121 100644 --- a/pylintrc +++ b/pylintrc @@ -1,6 +1,4 @@ -[MASTER] -reports=no - +[MESSAGES CONTROL] # Reasons disabled: # locally-disabled - it spams too much # duplicate-code - unavoidable @@ -14,9 +12,6 @@ reports=no # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # inconsistent-return-statements - doesn't handle raise - -generated-members=botocore.errorfactory - disable= abstract-class-little-used, abstract-class-not-used, @@ -39,9 +34,13 @@ disable= too-many-statements, unused-argument +[REPORTS] +reports=no + +[TYPECHECK] +# For attrs +ignored-classes=_CountingAttr +generated-members=botocore.errorfactory + [EXCEPTIONS] overgeneral-exceptions=Exception,HomeAssistantError - -# For attrs -[typecheck] -ignored-classes=_CountingAttr From 508d0459a7c188e87176e0ad57bed5d168e11050 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 25 Jun 2018 10:03:39 -0700 Subject: [PATCH 069/128] Fix #14919. Should throw exception when camera stream closed by frontend (#15028) * Fix #14919. Should throw exception when camera stream closed by frontend * Re-trigger CI * pythonic re-raise --- homeassistant/components/camera/__init__.py | 1 + homeassistant/components/camera/proxy.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index ebda09de20c..14550dab899 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -322,6 +322,7 @@ class Camera(Entity): except asyncio.CancelledError: _LOGGER.debug("Stream closed by frontend.") response = None + raise finally: if response is not None: diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index 1984c21fadb..447f4e1e56a 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -233,6 +233,7 @@ class ProxyCamera(Camera): _LOGGER.debug("Stream closed by frontend.") req.close() response = None + raise finally: if response is not None: From 42ba2a68ce47b4890328b83cf2ddcaf907380611 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 25 Jun 2018 19:04:07 +0200 Subject: [PATCH 070/128] Revert "Add language to dark sky weather component" (#15142) * Revert "Fix #14919. Should throw exception when camera stream closed by frontend (#15028)" This reverts commit 508d0459a7c188e87176e0ad57bed5d168e11050. * Revert "Fix pylintrc section order and option placements (#15120)" This reverts commit dbae410cf40f3a684ee5500b0691bed4c5c65204. * Revert "Add storage helper and migrate config entries (#15045)" This reverts commit ae51dc08bf5cb947d14b0e55fb64387b31924ac5. * Revert "Add language to dark sky weather component (#15130)" This reverts commit 672a3c7178afc91ac0da015f098dd5f96672e4f2. --- homeassistant/components/weather/darksky.py | 28 +++++---------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 86cc740edbc..f0712542ea5 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -25,22 +25,9 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by Dark Sky" -# Language Supported Codes -LANGUAGE_CODES = [ - 'ar', 'az', 'be', 'bg', 'bs', 'ca', - 'cs', 'da', 'de', 'el', 'en', 'es', - 'et', 'fi', 'fr', 'hr', 'hu', 'id', - 'is', 'it', 'ja', 'ka', 'kw', 'nb', - 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', - 'sl', 'sr', 'sv', 'tet', 'tr', 'uk', - 'x-pig-latin', 'zh', 'zh-tw', -] - CONF_UNITS = 'units' -CONF_LANGUAGE = 'language' DEFAULT_NAME = 'Dark Sky' -DEFAULT_LANGUAGE = 'en' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, @@ -48,8 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_LONGITUDE): cv.longitude, vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_LANGUAGE, - default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), }) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=3) @@ -59,15 +44,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Dark Sky weather.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - name = config[CONF_NAME] - lang = config[CONF_LANGUAGE] - api_key = config[CONF_API_KEY] + name = config.get(CONF_NAME) units = config.get(CONF_UNITS) if not units: units = 'si' if hass.config.units.is_metric else 'us' - dark_sky = DarkSkyData(api_key, latitude, longitude, units, lang) + dark_sky = DarkSkyData( + config.get(CONF_API_KEY), latitude, longitude, units) + add_devices([DarkSkyWeather(name, dark_sky)], True) @@ -147,13 +132,12 @@ class DarkSkyWeather(WeatherEntity): class DarkSkyData(object): """Get the latest data from Dark Sky.""" - def __init__(self, api_key, latitude, longitude, units, lang): + def __init__(self, api_key, latitude, longitude, units): """Initialize the data object.""" self._api_key = api_key self.latitude = latitude self.longitude = longitude self.requested_units = units - self.language = lang self.data = None self.currently = None @@ -168,7 +152,7 @@ class DarkSkyData(object): try: self.data = forecastio.load_forecast( self._api_key, self.latitude, self.longitude, - units=self.requested_units, lang=self.language) + units=self.requested_units) self.currently = self.data.currently() self.hourly = self.data.hourly() self.daily = self.data.daily() From 6c0fc65eaf66cea8f697e40823a39ae556aa639e Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 25 Jun 2018 10:04:32 -0700 Subject: [PATCH 071/128] Bump python-nest to 4.0.3 (#15098) Resolve network reconnect issue --- homeassistant/components/nest/__init__.py | 3 ++- homeassistant/components/sensor/nest.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index bd74897371a..f9507b6ec7b 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity import Entity from .const import DOMAIN from . import local_auth -REQUIREMENTS = ['python-nest==4.0.2'] +REQUIREMENTS = ['python-nest==4.0.3'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -86,6 +86,7 @@ async def async_nest_update_event_broker(hass, nest): _LOGGER.debug("dispatching nest data update") async_dispatcher_send(hass, SIGNAL_NEST_UPDATE) else: + _LOGGER.debug("stop listening nest.update_event") return diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index bf1b3f65c4a..7afd3b762b3 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -133,7 +133,8 @@ class NestBasicSensor(NestSensorDevice): elif self.variable in PROTECT_SENSOR_TYPES \ and self.variable != 'color_status': # keep backward compatibility - self._state = getattr(self.device, self.variable).capitalize() + state = getattr(self.device, self.variable) + self._state = state.capitalize() if state is not None else None else: self._state = getattr(self.device, self.variable) diff --git a/requirements_all.txt b/requirements_all.txt index 74ff9286803..78fda284b17 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1059,7 +1059,7 @@ python-mpd2==1.0.0 python-mystrom==0.4.4 # homeassistant.components.nest -python-nest==4.0.2 +python-nest==4.0.3 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6813378b12f..04952b75b81 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -156,7 +156,7 @@ pyqwikswitch==0.8 python-forecastio==1.4.0 # homeassistant.components.nest -python-nest==4.0.2 +python-nest==4.0.3 # homeassistant.components.sensor.whois pythonwhois==2.4.3 From b92350fb5588a05a870495753a0c0b3c69d12bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 25 Jun 2018 20:05:07 +0300 Subject: [PATCH 072/128] Lint cleanup (#15103) * Remove unneeded inline pylint disables * Remove unneeded noqa's * Use symbol names instead of message ids in inline pylint disables --- homeassistant/bootstrap.py | 1 - .../alarm_control_panel/__init__.py | 1 - homeassistant/components/alexa/smart_home.py | 4 ---- homeassistant/components/api.py | 1 - homeassistant/components/bbb_gpio.py | 19 ++++++++----------- .../components/binary_sensor/__init__.py | 1 - .../binary_sensor/bmw_connected_drive.py | 6 +++--- .../components/binary_sensor/gc100.py | 1 - .../components/binary_sensor/isy994.py | 2 +- .../components/binary_sensor/rpi_gpio.py | 1 - .../components/binary_sensor/wemo.py | 1 - homeassistant/components/calendar/google.py | 4 +--- homeassistant/components/camera/xeoma.py | 2 -- homeassistant/components/climate/__init__.py | 1 - .../components/climate/eq3btsmart.py | 2 +- .../components/climate/generic_thermostat.py | 2 -- homeassistant/components/climate/heatmiser.py | 1 - homeassistant/components/climate/mqtt.py | 2 -- homeassistant/components/climate/zwave.py | 1 - homeassistant/components/cover/__init__.py | 1 - homeassistant/components/cover/demo.py | 1 - homeassistant/components/cover/garadget.py | 1 - homeassistant/components/cover/isy994.py | 2 +- homeassistant/components/cover/opengarage.py | 1 - homeassistant/components/cover/zwave.py | 1 - .../components/device_tracker/cisco_ios.py | 1 - .../components/device_tracker/gpslogger.py | 2 +- .../components/device_tracker/linksys_ap.py | 1 - .../components/device_tracker/snmp.py | 3 --- .../components/device_tracker/tplink.py | 5 ----- homeassistant/components/ecobee.py | 1 - homeassistant/components/gc100.py | 2 +- homeassistant/components/google.py | 2 +- .../components/google_assistant/__init__.py | 5 ++--- .../components/google_assistant/auth.py | 7 +++---- .../components/google_assistant/http.py | 4 ++-- .../components/google_assistant/smart_home.py | 2 +- .../components/homekit_controller/__init__.py | 1 - homeassistant/components/http/static.py | 1 - .../components/image_processing/opencv.py | 2 -- homeassistant/components/ios.py | 2 -- homeassistant/components/isy994.py | 5 ++--- homeassistant/components/keyboard_remote.py | 1 - homeassistant/components/konnected.py | 2 +- homeassistant/components/lametric.py | 1 - homeassistant/components/light/__init__.py | 2 -- homeassistant/components/light/avion.py | 6 +++--- homeassistant/components/light/blinkt.py | 2 +- homeassistant/components/light/decora.py | 2 +- homeassistant/components/light/decora_wifi.py | 2 +- .../components/light/limitlessled.py | 2 +- homeassistant/components/light/zwave.py | 2 -- homeassistant/components/lirc.py | 2 +- homeassistant/components/lock/__init__.py | 1 - homeassistant/components/lock/isy994.py | 2 +- homeassistant/components/lock/sesame.py | 2 +- homeassistant/components/lock/zwave.py | 2 -- homeassistant/components/logger.py | 1 - .../components/media_player/__init__.py | 1 - homeassistant/components/media_player/cast.py | 1 - homeassistant/components/media_player/demo.py | 1 - .../components/media_player/denonavr.py | 1 - homeassistant/components/media_player/plex.py | 1 - .../components/media_player/universal.py | 1 - homeassistant/components/modbus.py | 1 - homeassistant/components/notify/aws_lambda.py | 1 - homeassistant/components/notify/aws_sns.py | 1 - homeassistant/components/notify/aws_sqs.py | 1 - homeassistant/components/notify/ciscospark.py | 1 - homeassistant/components/notify/ecobee.py | 2 +- homeassistant/components/notify/html5.py | 1 - .../components/notify/joaoapps_join.py | 1 - homeassistant/components/notify/lametric.py | 2 -- homeassistant/components/notify/pushover.py | 1 - homeassistant/components/notify/slack.py | 1 - homeassistant/components/octoprint.py | 1 - .../components/remote/xiaomi_miio.py | 1 - homeassistant/components/rfxtrx.py | 2 -- homeassistant/components/rpi_gpio.py | 1 - homeassistant/components/satel_integra.py | 1 - homeassistant/components/sensor/bitcoin.py | 1 - homeassistant/components/sensor/buienradar.py | 5 +---- homeassistant/components/sensor/cpuspeed.py | 1 - homeassistant/components/sensor/cups.py | 2 +- .../components/sensor/dwd_weather_warnings.py | 2 -- homeassistant/components/sensor/dweet.py | 1 - homeassistant/components/sensor/envirophat.py | 2 -- homeassistant/components/sensor/glances.py | 1 - homeassistant/components/sensor/gpsd.py | 1 - homeassistant/components/sensor/isy994.py | 2 +- homeassistant/components/sensor/kira.py | 1 - homeassistant/components/sensor/lastfm.py | 1 - homeassistant/components/sensor/loopenergy.py | 1 - homeassistant/components/sensor/mfi.py | 1 - homeassistant/components/sensor/pvoutput.py | 1 - homeassistant/components/sensor/skybeacon.py | 3 +-- homeassistant/components/sensor/sma.py | 3 +-- .../components/sensor/steam_online.py | 1 - homeassistant/components/sensor/tado.py | 1 - homeassistant/components/sensor/ted5000.py | 1 - .../components/sensor/wirelesstag.py | 2 +- homeassistant/components/sensor/yr.py | 2 +- homeassistant/components/sensor/zwave.py | 2 -- homeassistant/components/switch/__init__.py | 1 - .../components/switch/anel_pwrctrl.py | 1 - homeassistant/components/switch/flux.py | 1 - homeassistant/components/switch/fritzdect.py | 2 +- homeassistant/components/switch/gc100.py | 1 - homeassistant/components/switch/isy994.py | 4 ++-- homeassistant/components/switch/mfi.py | 1 - homeassistant/components/switch/rpi_rf.py | 2 +- homeassistant/components/switch/wemo.py | 1 - homeassistant/components/switch/zwave.py | 2 -- homeassistant/components/vacuum/demo.py | 1 - homeassistant/components/vacuum/mqtt.py | 1 - homeassistant/components/vera.py | 1 - homeassistant/components/weather/__init__.py | 1 - homeassistant/components/websocket_api.py | 1 - homeassistant/components/wemo.py | 1 - homeassistant/components/wirelesstag.py | 2 +- homeassistant/components/zoneminder.py | 3 --- homeassistant/components/zwave/__init__.py | 1 - homeassistant/components/zwave/const.py | 1 - homeassistant/helpers/__init__.py | 3 +-- homeassistant/helpers/aiohttp_client.py | 1 - homeassistant/helpers/entity.py | 2 -- homeassistant/helpers/entity_registry.py | 2 -- homeassistant/helpers/event.py | 1 - homeassistant/remote.py | 1 - homeassistant/scripts/check_config.py | 3 +-- homeassistant/util/__init__.py | 1 - homeassistant/util/async_.py | 2 -- homeassistant/util/color.py | 13 ++----------- homeassistant/util/dt.py | 1 - homeassistant/util/location.py | 2 +- homeassistant/util/yaml.py | 3 +-- script/lazytox.py | 2 -- 137 files changed, 58 insertions(+), 209 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b108ac805e9..0a71c2887b1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -123,7 +123,6 @@ async def async_from_config_dict(config: Dict[str, Any], components.update(hass.config_entries.async_domains()) # setup components - # pylint: disable=not-an-iterable res = await core_components.async_setup(hass, config) if not res: _LOGGER.error("Home Assistant core failed to initialize. " diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 25e303cbe85..f81d2ef1037 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -154,7 +154,6 @@ def async_setup(hass, config): return True -# pylint: disable=no-self-use class AlarmControlPanel(Entity): """An abstract class for alarm control devices.""" diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index c5c68f1af40..ff2d4adf30d 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -107,7 +107,6 @@ class _DisplayCategory(object): THERMOSTAT = "THERMOSTAT" # Indicates the endpoint is a television. - # pylint: disable=invalid-name TV = "TV" @@ -1474,9 +1473,6 @@ async def async_api_set_thermostat_mode(hass, config, request, entity): mode = mode if isinstance(mode, str) else mode['value'] operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST) - # Work around a pylint false positive due to - # https://github.com/PyCQA/pylint/issues/1830 - # pylint: disable=stop-iteration-return ha_mode = next( (k for k, v in API_THERMOSTAT_MODES.items() if v == mode), None diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index ae89e2fc3b6..b80a5716061 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -81,7 +81,6 @@ class APIEventStream(HomeAssistantView): async def get(self, request): """Provide a streaming interface for the event bus.""" - # pylint: disable=no-self-use hass = request.app['hass'] stop_obj = object() to_write = asyncio.Queue(loop=hass.loop) diff --git a/homeassistant/components/bbb_gpio.py b/homeassistant/components/bbb_gpio.py index 5d3954b4c87..f932f239969 100644 --- a/homeassistant/components/bbb_gpio.py +++ b/homeassistant/components/bbb_gpio.py @@ -16,7 +16,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'bbb_gpio' -# pylint: disable=no-member def setup(hass, config): """Set up the BeagleBone Black GPIO component.""" # pylint: disable=import-error @@ -34,41 +33,39 @@ def setup(hass, config): return True -# noqa: F821 - def setup_output(pin): """Set up a GPIO as output.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO GPIO.setup(pin, GPIO.OUT) def setup_input(pin, pull_mode): """Set up a GPIO as input.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO - GPIO.setup(pin, GPIO.IN, # noqa: F821 - GPIO.PUD_DOWN if pull_mode == 'DOWN' # noqa: F821 - else GPIO.PUD_UP) # noqa: F821 + GPIO.setup(pin, GPIO.IN, + GPIO.PUD_DOWN if pull_mode == 'DOWN' + else GPIO.PUD_UP) def write_output(pin, value): """Write a value to a GPIO.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO GPIO.output(pin, value) def read_input(pin): """Read a value from a GPIO.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO return GPIO.input(pin) is GPIO.HIGH def edge_detect(pin, event_callback, bounce): """Add detection for RISING and FALLING events.""" - # pylint: disable=import-error,undefined-variable + # pylint: disable=import-error import Adafruit_BBIO.GPIO as GPIO GPIO.add_event_detect( pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index d72211d5ad1..26878044fe2 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -67,7 +67,6 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -# pylint: disable=no-self-use class BinarySensorDevice(Entity): """Represent a binary sensor.""" diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index e214610f46d..308298d1bcd 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -124,11 +124,11 @@ class BMWConnectedDriveSensor(BinarySensorDevice): result['check_control_messages'] = check_control_messages elif self._attribute == 'charging_status': result['charging_status'] = vehicle_state.charging_status.value - # pylint: disable=W0212 + # pylint: disable=protected-access result['last_charging_end_result'] = \ vehicle_state._attributes['lastChargingEndResult'] if self._attribute == 'connection_status': - # pylint: disable=W0212 + # pylint: disable=protected-access result['connection_status'] = \ vehicle_state._attributes['connectionStatus'] @@ -166,7 +166,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): # device class plug: On means device is plugged in, # Off means device is unplugged if self._attribute == 'connection_status': - # pylint: disable=W0212 + # pylint: disable=protected-access self._state = (vehicle_state._attributes['connectionStatus'] == 'CONNECTED') diff --git a/homeassistant/components/binary_sensor/gc100.py b/homeassistant/components/binary_sensor/gc100.py index 767be2874e6..515d7e7123d 100644 --- a/homeassistant/components/binary_sensor/gc100.py +++ b/homeassistant/components/binary_sensor/gc100.py @@ -39,7 +39,6 @@ class GC100BinarySensor(BinarySensorDevice): def __init__(self, name, port_addr, gc100): """Initialize the GC100 binary sensor.""" - # pylint: disable=no-member self._name = name or DEVICE_DEFAULT_NAME self._port_addr = port_addr self._gc100 = gc100 diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index a80e4db747d..deaa118f51c 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -8,7 +8,7 @@ https://home-assistant.io/components/binary_sensor.isy994/ import asyncio import logging from datetime import timedelta -from typing import Callable # noqa +from typing import Callable from homeassistant.core import callback from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py index e1e06ce57b9..4072f4ae234 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -58,7 +58,6 @@ class RPiGPIOBinarySensor(BinarySensorDevice): def __init__(self, name, port, pull_mode, bouncetime, invert_logic): """Initialize the RPi binary sensor.""" - # pylint: disable=no-member self._name = name or DEVICE_DEFAULT_NAME self._port = port self._pull_mode = pull_mode diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index d3c78597c70..e6eff0d9bb5 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -13,7 +13,6 @@ DEPENDENCIES = ['wemo'] _LOGGER = logging.getLogger(__name__) -# pylint: disable=too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Register discovered WeMo binary sensors.""" import pywemo.discovery as discovery diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index da76530a36d..87893125e6f 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -89,9 +89,7 @@ class GoogleCalendarData(object): params['timeMin'] = start_date.isoformat('T') params['timeMax'] = end_date.isoformat('T') - # pylint: disable=no-member events = await hass.async_add_job(service.events) - # pylint: enable=no-member result = await hass.async_add_job(events.list(**params).execute) items = result.get('items', []) @@ -111,7 +109,7 @@ class GoogleCalendarData(object): service, params = self._prepare_query() params['timeMin'] = dt.now().isoformat('T') - events = service.events() # pylint: disable=no-member + events = service.events() result = events.list(**params).execute() items = result.get('items', []) diff --git a/homeassistant/components/camera/xeoma.py b/homeassistant/components/camera/xeoma.py index cec04b52047..2a4d1526818 100644 --- a/homeassistant/components/camera/xeoma.py +++ b/homeassistant/components/camera/xeoma.py @@ -67,8 +67,6 @@ async def async_setup_platform(hass, config, async_add_devices, ] for cam in config.get(CONF_CAMERAS, []): - # https://github.com/PyCQA/pylint/issues/1830 - # pylint: disable=stop-iteration-return camera = next( (dc for dc in discovered_cameras if dc[CONF_IMAGE_NAME] == cam[CONF_IMAGE_NAME]), None) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index a47edc5af42..9584422e2b4 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -470,7 +470,6 @@ async def async_unload_entry(hass, entry): class ClimateDevice(Entity): """Representation of a climate device.""" - # pylint: disable=no-self-use @property def state(self): """Return the current state.""" diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 820e715b00d..10fd879e386 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) -# pylint: disable=import-error, no-name-in-module +# pylint: disable=import-error class EQ3BTSmartThermostat(ClimateDevice): """Representation of an eQ-3 Bluetooth Smart thermostat.""" diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 030a76626c6..3f1d9a208ac 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -263,7 +263,6 @@ class GenericThermostat(ClimateDevice): @property def min_temp(self): """Return the minimum temperature.""" - # pylint: disable=no-member if self._min_temp: return self._min_temp @@ -273,7 +272,6 @@ class GenericThermostat(ClimateDevice): @property def max_temp(self): """Return the maximum temperature.""" - # pylint: disable=no-member if self._max_temp: return self._max_temp diff --git a/homeassistant/components/climate/heatmiser.py b/homeassistant/components/climate/heatmiser.py index 19c033a319f..92e363228a8 100644 --- a/homeassistant/components/climate/heatmiser.py +++ b/homeassistant/components/climate/heatmiser.py @@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the heatmiser thermostat.""" from heatmiserV3 import heatmiser, connection diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 2878717d91b..fbe5460979b 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -638,11 +638,9 @@ class MqttClimate(MqttAvailability, ClimateDevice): @property def min_temp(self): """Return the minimum temperature.""" - # pylint: disable=no-member return self._min_temp @property def max_temp(self): """Return the maximum temperature.""" - # pylint: disable=no-member return self._max_temp diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 1eec9c82f3c..c87d1507e92 100644 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.zwave/ """ # Because we do not compile openzwave on CI -# pylint: disable=import-error import logging from homeassistant.components.climate import ( DOMAIN, ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index e4c8f5634cf..f5d3d798e2e 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -198,7 +198,6 @@ async def async_setup(hass, config): class CoverDevice(Entity): """Representation a cover.""" - # pylint: disable=no-self-use @property def current_cover_position(self): """Return current position of cover. diff --git a/homeassistant/components/cover/demo.py b/homeassistant/components/cover/demo.py index 70e681f1120..b1533bd68c8 100644 --- a/homeassistant/components/cover/demo.py +++ b/homeassistant/components/cover/demo.py @@ -24,7 +24,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DemoCover(CoverDevice): """Representation of a demo cover.""" - # pylint: disable=no-self-use def __init__(self, hass, name, position=None, tilt_position=None, device_class=None, supported_features=None): """Initialize the cover.""" diff --git a/homeassistant/components/cover/garadget.py b/homeassistant/components/cover/garadget.py index c19aa69c8f0..70f69568109 100644 --- a/homeassistant/components/cover/garadget.py +++ b/homeassistant/components/cover/garadget.py @@ -73,7 +73,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class GaradgetCover(CoverDevice): """Representation of a Garadget cover.""" - # pylint: disable=no-self-use def __init__(self, hass, args): """Initialize the cover.""" self.particle_url = 'https://api.particle.io' diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py index 743a36d41d5..0ccfe267989 100644 --- a/homeassistant/components/cover/isy994.py +++ b/homeassistant/components/cover/isy994.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.isy994/ """ import logging -from typing import Callable # noqa +from typing import Callable from homeassistant.components.cover import CoverDevice, DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, diff --git a/homeassistant/components/cover/opengarage.py b/homeassistant/components/cover/opengarage.py index 028a7a0c9fc..fe6c7763cc7 100644 --- a/homeassistant/components/cover/opengarage.py +++ b/homeassistant/components/cover/opengarage.py @@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class OpenGarageCover(CoverDevice): """Representation of a OpenGarage cover.""" - # pylint: disable=no-self-use def __init__(self, hass, args): """Initialize the cover.""" self.opengarage_url = 'http://{}:{}'.format( diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index 6f4a11684bd..c29c11c5b6b 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -42,7 +42,6 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): def __init__(self, hass, values, invert_buttons): """Initialize the Z-Wave rollershutter.""" ZWaveDeviceEntity.__init__(self, values, DOMAIN) - # pylint: disable=no-member self._network = hass.data[zwave.const.DATA_NETWORK] self._open_id = None self._close_id = None diff --git a/homeassistant/components/device_tracker/cisco_ios.py b/homeassistant/components/device_tracker/cisco_ios.py index 0978ba99593..c13f622c5bf 100644 --- a/homeassistant/components/device_tracker/cisco_ios.py +++ b/homeassistant/components/device_tracker/cisco_ios.py @@ -50,7 +50,6 @@ class CiscoDeviceScanner(DeviceScanner): self.success_init = self._update_info() _LOGGER.info('cisco_ios scanner initialized') - # pylint: disable=no-self-use def get_device_name(self, device): """Get the firmware doesn't save the name of the wireless device.""" return None diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index 68ea9ac88ae..6336ba51d23 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/device_tracker.gpslogger/ import logging from hmac import compare_digest -from aiohttp.web import Request, HTTPUnauthorized # NOQA +from aiohttp.web import Request, HTTPUnauthorized import voluptuous as vol import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/device_tracker/linksys_ap.py b/homeassistant/components/device_tracker/linksys_ap.py index 8837b628b32..bf3916f3abe 100644 --- a/homeassistant/components/device_tracker/linksys_ap.py +++ b/homeassistant/components/device_tracker/linksys_ap.py @@ -61,7 +61,6 @@ class LinksysAPDeviceScanner(DeviceScanner): return self.last_results - # pylint: disable=no-self-use def get_device_name(self, device): """ Return the name (if known) of the device. diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 3d57cb108e2..6a849d0b05a 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -74,8 +74,6 @@ class SnmpScanner(DeviceScanner): return [client['mac'] for client in self.last_results if client.get('mac')] - # Suppressing no-self-use warning - # pylint: disable=R0201 def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" # We have no names @@ -106,7 +104,6 @@ class SnmpScanner(DeviceScanner): if errindication: _LOGGER.error("SNMPLIB error: %s", errindication) return - # pylint: disable=no-member if errstatus: _LOGGER.error("SNMP error: %s at %s", errstatus.prettyPrint(), errindex and restable[int(errindex) - 1][0] or '?') diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index 6c5fb697c07..5266b9c6f57 100644 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -68,7 +68,6 @@ class TplinkDeviceScanner(DeviceScanner): self._update_info() return self.last_results - # pylint: disable=no-self-use def get_device_name(self, device): """Get firmware doesn't save the name of the wireless device.""" return None @@ -103,7 +102,6 @@ class Tplink2DeviceScanner(TplinkDeviceScanner): self._update_info() return self.last_results.keys() - # pylint: disable=no-self-use def get_device_name(self, device): """Get firmware doesn't save the name of the wireless device.""" return self.last_results.get(device) @@ -164,7 +162,6 @@ class Tplink3DeviceScanner(TplinkDeviceScanner): self._log_out() return self.last_results.keys() - # pylint: disable=no-self-use def get_device_name(self, device): """Get the firmware doesn't save the name of the wireless device. @@ -273,7 +270,6 @@ class Tplink4DeviceScanner(TplinkDeviceScanner): self._update_info() return self.last_results - # pylint: disable=no-self-use def get_device_name(self, device): """Get the name of the wireless device.""" return None @@ -349,7 +345,6 @@ class Tplink5DeviceScanner(TplinkDeviceScanner): self._update_info() return self.last_results.keys() - # pylint: disable=no-self-use def get_device_name(self, device): """Get firmware doesn't save the name of the wireless device.""" return None diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 22348dcc297..96f094b527d 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -105,7 +105,6 @@ def setup(hass, config): Will automatically load thermostat and sensor components to support devices discovered on the network. """ - # pylint: disable=import-error global NETWORK if 'ecobee' in _CONFIGURING: diff --git a/homeassistant/components/gc100.py b/homeassistant/components/gc100.py index bc627d44417..25bcb5b0f79 100644 --- a/homeassistant/components/gc100.py +++ b/homeassistant/components/gc100.py @@ -31,7 +31,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=no-member, import-self +# pylint: disable=no-member def setup(hass, base_config): """Set up the gc100 component.""" import gc100 diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py index b41d4ea33a2..203b1a94b7f 100644 --- a/homeassistant/components/google.py +++ b/homeassistant/components/google.py @@ -197,7 +197,7 @@ def setup_services(hass, track_new_found_calendars, calendar_service): def _scan_for_calendars(service): """Scan for new calendars.""" service = calendar_service.get() - cal_list = service.calendarList() # pylint: disable=no-member + cal_list = service.calendarList() calendars = cal_list.list().execute()['items'] for calendar in calendars: calendar['track'] = track_new_found_calendars diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 1c6d11a7c99..567a6d84233 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -13,9 +13,8 @@ import async_timeout import voluptuous as vol # Typing imports -# pylint: disable=using-constant-test,unused-import,ungrouped-imports -from homeassistant.core import HomeAssistant # NOQA -from typing import Dict, Any # NOQA +from homeassistant.core import HomeAssistant +from typing import Dict, Any from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/google_assistant/auth.py b/homeassistant/components/google_assistant/auth.py index a21dd0e6738..e80b2282066 100644 --- a/homeassistant/components/google_assistant/auth.py +++ b/homeassistant/components/google_assistant/auth.py @@ -3,12 +3,11 @@ import logging # Typing imports -# pylint: disable=using-constant-test,unused-import,ungrouped-imports # if False: -from aiohttp.web import Request, Response # NOQA -from typing import Dict, Any # NOQA +from aiohttp.web import Request, Response +from typing import Dict, Any -from homeassistant.core import HomeAssistant # NOQA +from homeassistant.core import HomeAssistant from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( HTTP_BAD_REQUEST, diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 0ea5f7d9fa4..65079a1a26e 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -7,10 +7,10 @@ https://home-assistant.io/components/google_assistant/ import logging from aiohttp.hdrs import AUTHORIZATION -from aiohttp.web import Request, Response # NOQA +from aiohttp.web import Request, Response # Typing imports -# pylint: disable=using-constant-test,unused-import,ungrouped-imports +# pylint: disable=unused-import from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback # NOQA from homeassistant.helpers.entity import Entity # NOQA diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 27d993aee76..f20d4f747cc 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -4,7 +4,7 @@ from itertools import product import logging # Typing imports -# pylint: disable=using-constant-test,unused-import,ungrouped-imports +# pylint: disable=unused-import # if False: from aiohttp.web import Request, Response # NOQA from typing import Dict, Tuple, Any, Optional # NOQA diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index ff981c1607a..34fdcb2c035 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -226,7 +226,6 @@ class HomeKitEntity(Entity): self._securecon.put('/characteristics', body) -# pylint: too-many-function-args def setup(hass, config): """Set up for Homekit devices.""" def discovery_dispatch(service, discovery_info): diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 3fbaf703d06..cd07ab6df69 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -18,7 +18,6 @@ class CachingStaticResource(StaticResource): filename = URL(request.match_info['filename']).path try: # PyLint is wrong about resolve not being a member. - # pylint: disable=no-member filepath = self._directory.joinpath(filename).resolve() if not self._follow_symlinks: filepath.relative_to(self._directory) diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index e01131c7d1b..ca0f3527f73 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -152,7 +152,6 @@ class OpenCVImageProcessor(ImageProcessingEntity): import cv2 # pylint: disable=import-error import numpy - # pylint: disable=no-member cv_image = cv2.imdecode( numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) @@ -168,7 +167,6 @@ class OpenCVImageProcessor(ImageProcessingEntity): else: path = classifier - # pylint: disable=no-member cascade = cv2.CascadeClassifier(path) detections = cascade.detectMultiScale( diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index 249f147847c..7f7377469fd 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -181,7 +181,6 @@ def devices_with_push(): def enabled_push_ids(): """Return a list of push enabled target push IDs.""" push_ids = list() - # pylint: disable=unused-variable for device in CONFIG_FILE[ATTR_DEVICES].values(): if device.get(ATTR_PUSH_ID) is not None: push_ids.append(device.get(ATTR_PUSH_ID)) @@ -203,7 +202,6 @@ def device_name_for_push_id(push_id): def setup(hass, config): """Set up the iOS component.""" - # pylint: disable=import-error global CONFIG_FILE global CONFIG_FILE_PATH diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 90ab41cf98b..d8afb7be5da 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -11,12 +11,12 @@ from urllib.parse import urlparse import voluptuous as vol -from homeassistant.core import HomeAssistant # noqa +from homeassistant.core import HomeAssistant from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery, config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, Dict # noqa +from homeassistant.helpers.typing import ConfigType, Dict REQUIREMENTS = ['PyISY==1.1.0'] @@ -268,7 +268,6 @@ def _is_sensor_a_binary_sensor(hass: HomeAssistant, node) -> bool: def _categorize_nodes(hass: HomeAssistant, nodes, ignore_identifier: str, sensor_identifier: str)-> None: """Sort the nodes to their proper domains.""" - # pylint: disable=no-member for (path, node) in nodes: ignored = ignore_identifier in path or ignore_identifier in node.name if ignored: diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote.py index af45bd3d4f9..bbd7bc44082 100644 --- a/homeassistant/components/keyboard_remote.py +++ b/homeassistant/components/keyboard_remote.py @@ -151,7 +151,6 @@ class KeyboardRemoteThread(threading.Thread): if not event: continue - # pylint: disable=no-member if event.type is ecodes.EV_KEY and event.value is self.key_value: _LOGGER.debug(categorize(event)) self.hass.bus.fire( diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 5b28b7b0999..26fe356d772 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -10,7 +10,7 @@ import json import voluptuous as vol from aiohttp.hdrs import AUTHORIZATION -from aiohttp.web import Request, Response # NOQA +from aiohttp.web import Request, Response from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA from homeassistant.components.discovery import SERVICE_KONNECTED diff --git a/homeassistant/components/lametric.py b/homeassistant/components/lametric.py index 49b4f73ea17..96ea3781566 100644 --- a/homeassistant/components/lametric.py +++ b/homeassistant/components/lametric.py @@ -31,7 +31,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=broad-except def setup(hass, config): """Set up the LaMetricManager.""" _LOGGER.debug("Setting up LaMetric platform") diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 30a1a800a44..b8a97607215 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -446,8 +446,6 @@ class Profiles: class Light(ToggleEntity): """Representation of a light.""" - # pylint: disable=no-self-use - @property def brightness(self): """Return the brightness of this light between 0..255.""" diff --git a/homeassistant/components/light/avion.py b/homeassistant/components/light/avion.py index b4b9f4e7775..be608ea4776 100644 --- a/homeassistant/components/light/avion.py +++ b/homeassistant/components/light/avion.py @@ -37,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up an Avion switch.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import avion lights = [] @@ -70,7 +70,7 @@ class AvionLight(Light): def __init__(self, device): """Initialize the light.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import avion self._name = device['name'] @@ -117,7 +117,7 @@ class AvionLight(Light): def set_state(self, brightness): """Set the state of this lamp to the provided brightness.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import avion # Bluetooth LE is unreliable, and the connection may drop at any diff --git a/homeassistant/components/light/blinkt.py b/homeassistant/components/light/blinkt.py index 97edd7c54d2..7035320945a 100644 --- a/homeassistant/components/light/blinkt.py +++ b/homeassistant/components/light/blinkt.py @@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Blinkt Light platform.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import blinkt # ensure that the lights are off when exiting diff --git a/homeassistant/components/light/decora.py b/homeassistant/components/light/decora.py index c7478b435ee..85d9180c59b 100644 --- a/homeassistant/components/light/decora.py +++ b/homeassistant/components/light/decora.py @@ -75,7 +75,7 @@ class DecoraLight(Light): def __init__(self, device): """Initialize the light.""" - # pylint: disable=import-error, no-member + # pylint: disable=no-member import decora self._name = device['name'] diff --git a/homeassistant/components/light/decora_wifi.py b/homeassistant/components/light/decora_wifi.py index 111d39f2019..17003d51610 100644 --- a/homeassistant/components/light/decora_wifi.py +++ b/homeassistant/components/light/decora_wifi.py @@ -36,7 +36,7 @@ NOTIFICATION_TITLE = 'myLeviton Decora Setup' def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Decora WiFi platform.""" - # pylint: disable=import-error, no-member, no-name-in-module + # pylint: disable=import-error, no-name-in-module from decora_wifi import DecoraWiFiSession from decora_wifi.models.person import Person from decora_wifi.models.residential_account import ResidentialAccount diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index bd4fece89e3..71d3f9d95d7 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -136,7 +136,7 @@ def state(new_state): """ def decorator(function): """Set up the decorator function.""" - # pylint: disable=no-member,protected-access + # pylint: disable=protected-access def wrapper(self, **kwargs): """Wrap a group state change.""" from limitlessled.pipeline import Pipeline diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 04216780c80..3bfa167f8ec 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -6,8 +6,6 @@ https://home-assistant.io/components/light.zwave/ """ import logging -# Because we do not compile openzwave on CI -# pylint: disable=import-error from threading import Timer from homeassistant.components.light import ( ATTR_WHITE_VALUE, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, diff --git a/homeassistant/components/lirc.py b/homeassistant/components/lirc.py index 0cd49ab6c9a..d7ec49e0096 100644 --- a/homeassistant/components/lirc.py +++ b/homeassistant/components/lirc.py @@ -4,7 +4,7 @@ LIRC interface to receive signals from an infrared remote control. For more details about this component, please refer to the documentation at https://home-assistant.io/components/lirc/ """ -# pylint: disable=import-error,no-member +# pylint: disable=no-member import threading import time import logging diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index b3e4ac8f0ff..f03d028a38f 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -145,7 +145,6 @@ class LockDevice(Entity): """Last change triggered by.""" return None - # pylint: disable=no-self-use @property def code_format(self): """Regex for code format or None if no code is required.""" diff --git a/homeassistant/components/lock/isy994.py b/homeassistant/components/lock/isy994.py index 79e4308dbda..9bcf5a86d08 100644 --- a/homeassistant/components/lock/isy994.py +++ b/homeassistant/components/lock/isy994.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/lock.isy994/ """ import logging -from typing import Callable # noqa +from typing import Callable from homeassistant.components.lock import LockDevice, DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, diff --git a/homeassistant/components/lock/sesame.py b/homeassistant/components/lock/sesame.py index 09f7266d15c..8d9c05e3f26 100644 --- a/homeassistant/components/lock/sesame.py +++ b/homeassistant/components/lock/sesame.py @@ -4,7 +4,7 @@ Support for Sesame, by CANDY HOUSE. For more details about this platform, please refer to the documentation https://home-assistant.io/components/lock.sesame/ """ -from typing import Callable # noqa +from typing import Callable import voluptuous as vol import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 8f39d440cae..b7bc9f15e19 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -4,8 +4,6 @@ Z-Wave platform that handles simple door locks. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/lock.zwave/ """ -# Because we do not compile openzwave on CI -# pylint: disable=import-error import asyncio import logging diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index daaffd0174c..0baca2f341c 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -55,7 +55,6 @@ def set_level(hass, logs): class HomeAssistantLogFilter(logging.Filter): """A log filter.""" - # pylint: disable=no-init def __init__(self, logfilter): """Initialize the filter.""" super().__init__() diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index d963deba7b5..d314dec65ea 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -471,7 +471,6 @@ class MediaPlayerDevice(Entity): _access_token = None - # pylint: disable=no-self-use # Implement these for your media player @property def state(self): diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index eced0dbbe25..be7b635f863 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -4,7 +4,6 @@ Provide functionality to interact with Cast devices on the network. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.cast/ """ -# pylint: disable=import-error import logging import threading from typing import Optional, Tuple diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 405c220c877..9edf69cd9c6 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -295,7 +295,6 @@ class DemoMusicPlayer(AbstractDemoPlayer): @property def media_album_name(self): """Return the album of current playing media (Music track only).""" - # pylint: disable=no-self-use return "Bounzz" @property diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 8cd47476058..ff0e4d907b1 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -61,7 +61,6 @@ NewHost = namedtuple('NewHost', ['host', 'name']) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Denon platform.""" - # pylint: disable=import-error import denonavr # Initialize list with receivers to be started diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 6690382846f..ca6b9722a49 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -747,7 +747,6 @@ class PlexClient(MediaPlayerDevice): if self.device and 'playback' in self._device_protocol_capabilities: self.device.skipPrevious(self._active_media_plexapi_type) - # pylint: disable=W0613 def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" if not (self.device and diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 03f847ae40c..66d12190320 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.universal/ """ import logging -# pylint: disable=import-error from copy import copy import voluptuous as vol diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py index fe46c858b51..fc6db96e029 100644 --- a/homeassistant/components/modbus.py +++ b/homeassistant/components/modbus.py @@ -75,7 +75,6 @@ HUB = None def setup(hass, config): """Set up Modbus component.""" # Modbus connection type - # pylint: disable=import-error client_type = config[DOMAIN][CONF_TYPE] # Connect to Modbus network diff --git a/homeassistant/components/notify/aws_lambda.py b/homeassistant/components/notify/aws_lambda.py index b0cc4a0121d..46ac2f89d33 100644 --- a/homeassistant/components/notify/aws_lambda.py +++ b/homeassistant/components/notify/aws_lambda.py @@ -44,7 +44,6 @@ def get_service(hass, config, discovery_info=None): context_b64 = base64.b64encode(context_str.encode('utf-8')) context = context_b64.decode('utf-8') - # pylint: disable=import-error import boto3 aws_config = config.copy() diff --git a/homeassistant/components/notify/aws_sns.py b/homeassistant/components/notify/aws_sns.py index c94e3abaa96..7ecf5a7cc7f 100644 --- a/homeassistant/components/notify/aws_sns.py +++ b/homeassistant/components/notify/aws_sns.py @@ -35,7 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_service(hass, config, discovery_info=None): """Get the AWS SNS notification service.""" - # pylint: disable=import-error import boto3 aws_config = config.copy() diff --git a/homeassistant/components/notify/aws_sqs.py b/homeassistant/components/notify/aws_sqs.py index 43c04ed16d0..30b673846e7 100644 --- a/homeassistant/components/notify/aws_sqs.py +++ b/homeassistant/components/notify/aws_sqs.py @@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_service(hass, config, discovery_info=None): """Get the AWS SQS notification service.""" - # pylint: disable=import-error import boto3 aws_config = config.copy() diff --git a/homeassistant/components/notify/ciscospark.py b/homeassistant/components/notify/ciscospark.py index 0bf184023d7..e83e0e9024f 100644 --- a/homeassistant/components/notify/ciscospark.py +++ b/homeassistant/components/notify/ciscospark.py @@ -25,7 +25,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the CiscoSpark notification service.""" return CiscoSparkNotificationService( diff --git a/homeassistant/components/notify/ecobee.py b/homeassistant/components/notify/ecobee.py index c718149b4b5..31e4c4751c8 100644 --- a/homeassistant/components/notify/ecobee.py +++ b/homeassistant/components/notify/ecobee.py @@ -11,7 +11,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components import ecobee from homeassistant.components.notify import ( - BaseNotificationService, PLATFORM_SCHEMA) # NOQA + BaseNotificationService, PLATFORM_SCHEMA) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 7ccf4f8db90..7529608387d 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -413,7 +413,6 @@ class HTML5NotificationService(BaseNotificationService): json.dumps(payload), gcm_key=gcm_key, ttl='86400' ) - # pylint: disable=no-member if response.status_code == 410: _LOGGER.info("Notification channel has expired") reg = self.registrations.pop(target) diff --git a/homeassistant/components/notify/joaoapps_join.py b/homeassistant/components/notify/joaoapps_join.py index e391d6559e5..a75ff9cd165 100644 --- a/homeassistant/components/notify/joaoapps_join.py +++ b/homeassistant/components/notify/joaoapps_join.py @@ -28,7 +28,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the Join notification service.""" api_key = config.get(CONF_API_KEY) diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/notify/lametric.py index f6c3e152b0a..0cc3a0213b3 100644 --- a/homeassistant/components/notify/lametric.py +++ b/homeassistant/components/notify/lametric.py @@ -36,7 +36,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the LaMetric notification service.""" hlmn = hass.data.get(LAMETRIC_DOMAIN) @@ -59,7 +58,6 @@ class LaMetricNotificationService(BaseNotificationService): self._priority = priority self._devices = [] - # pylint: disable=broad-except def send_message(self, message="", **kwargs): """Send a message to some LaMetric device.""" from lmnotify import SimpleFrame, Sound, Model diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index cd73bbba4bf..3ec0b27e7c4 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -26,7 +26,6 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the Pushover notification service.""" from pushover import InitError diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index b50260e4c61..d4c5a196a3f 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -44,7 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): """Get the Slack notification service.""" import slacker diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index 5caaa1b372d..c1059227f7a 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -144,7 +144,6 @@ class OctoPrintAPI(object): return response -# pylint: disable=unused-variable def get_value_from_json(json_dict, sensor_type, group, tool): """Return the value for sensor_type from the JSON.""" if group not in json_dict: diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 8a3e51b55b3..59a2dc861a6 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -229,7 +229,6 @@ class XiaomiMiioRemote(RemoteDevice): return {'hidden': 'true'} return - # pylint: disable=R0201 @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the device on.""" diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 2f170a20646..afe777ff7cc 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -162,7 +162,6 @@ def get_pt2262_cmd(device_id, data_bits): return hex(data[-1] & mask) -# pylint: disable=unused-variable def get_pt2262_device(device_id): """Look for the device which id matches the given device_id parameter.""" for device in RFX_DEVICES.values(): @@ -176,7 +175,6 @@ def get_pt2262_device(device_id): return None -# pylint: disable=unused-variable def find_possible_pt2262_device(device_id): """Look for the device which id matches the given device_id parameter.""" for dev_id, device in RFX_DEVICES.items(): diff --git a/homeassistant/components/rpi_gpio.py b/homeassistant/components/rpi_gpio.py index dfc60b5e45e..5cb7bb337ce 100644 --- a/homeassistant/components/rpi_gpio.py +++ b/homeassistant/components/rpi_gpio.py @@ -17,7 +17,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'rpi_gpio' -# pylint: disable=no-member def setup(hass, config): """Set up the Raspberry PI GPIO component.""" import RPi.GPIO as GPIO diff --git a/homeassistant/components/satel_integra.py b/homeassistant/components/satel_integra.py index 4b61ff15c08..4247855da39 100644 --- a/homeassistant/components/satel_integra.py +++ b/homeassistant/components/satel_integra.py @@ -4,7 +4,6 @@ Support for Satel Integra devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/satel_integra/ """ -# pylint: disable=invalid-name import asyncio import logging diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index 38d2226012c..bd23b9850f7 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -121,7 +121,6 @@ class BitcoinSensor(Entity): stats = self.data.stats ticker = self.data.ticker - # pylint: disable=no-member if self.type == 'exchangerate': self._state = ticker[self._currency].p15min self._unit_of_measurement = self._currency diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 590d5a8f1ce..10a96ded437 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -287,7 +287,6 @@ class BrSensor(Entity): img = condition.get(IMAGE, None) - # pylint: disable=protected-access if new_state != self._state or img != self._entity_picture: self._state = new_state self._entity_picture = img @@ -299,12 +298,10 @@ class BrSensor(Entity): # update nested precipitation forecast sensors nested = data.get(PRECIPITATION_FORECAST) self._timeframe = nested.get(TIMEFRAME) - # pylint: disable=protected-access self._state = nested.get(self.type[len(PRECIPITATION_FORECAST)+1:]) return True # update all other sensors - # pylint: disable=protected-access self._state = data.get(self.type) return True @@ -329,7 +326,7 @@ class BrSensor(Entity): return self._state @property - def should_poll(self): # pylint: disable=no-self-use + def should_poll(self): """No polling needed.""" return False diff --git a/homeassistant/components/sensor/cpuspeed.py b/homeassistant/components/sensor/cpuspeed.py index c39ae43aef0..c6a7106663f 100644 --- a/homeassistant/components/sensor/cpuspeed.py +++ b/homeassistant/components/sensor/cpuspeed.py @@ -30,7 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the CPU speed sensor.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/cups.py b/homeassistant/components/sensor/cups.py index 7c1d9fc3d49..6d55853d724 100644 --- a/homeassistant/components/sensor/cups.py +++ b/homeassistant/components/sensor/cups.py @@ -128,7 +128,7 @@ class CupsSensor(Entity): self._printer = self.data.printers.get(self._name) -# pylint: disable=import-error, no-name-in-module +# pylint: disable=no-name-in-module class CupsData(object): """Get the latest data from CUPS and update the state.""" diff --git a/homeassistant/components/sensor/dwd_weather_warnings.py b/homeassistant/components/sensor/dwd_weather_warnings.py index 9105e30eb42..e023dfcc49f 100644 --- a/homeassistant/components/sensor/dwd_weather_warnings.py +++ b/homeassistant/components/sensor/dwd_weather_warnings.py @@ -95,7 +95,6 @@ class DwdWeatherWarningsSensor(Entity): """Return the unit the value is expressed in.""" return self._var_units - # pylint: disable=no-member @property def state(self): """Return the state of the device.""" @@ -104,7 +103,6 @@ class DwdWeatherWarningsSensor(Entity): except TypeError: return self._api.data[self._var_id] - # pylint: disable=no-member @property def device_state_attributes(self): """Return the state attributes of the DWD-Weather-Warnings.""" diff --git a/homeassistant/components/sensor/dweet.py b/homeassistant/components/sensor/dweet.py index 157f366c0c4..cca06bd9782 100644 --- a/homeassistant/components/sensor/dweet.py +++ b/homeassistant/components/sensor/dweet.py @@ -34,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable, too-many-function-args def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Dweet sensor.""" import dweepy diff --git a/homeassistant/components/sensor/envirophat.py b/homeassistant/components/sensor/envirophat.py index b11dae8e168..265350f3e95 100644 --- a/homeassistant/components/sensor/envirophat.py +++ b/homeassistant/components/sensor/envirophat.py @@ -55,7 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Sense HAT sensor platform.""" try: - # pylint: disable=import-error import envirophat except OSError: _LOGGER.error("No Enviro pHAT was found.") @@ -175,7 +174,6 @@ class EnvirophatData(object): self.light_red, self.light_green, self.light_blue = \ self.envirophat.light.rgb() if self.use_leds: - # pylint: disable=no-value-for-parameter self.envirophat.leds.off() # accelerometer readings in G diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 4fed3793c50..bd6e91c7b53 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -56,7 +56,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Glances sensor.""" name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/gpsd.py b/homeassistant/components/sensor/gpsd.py index 472dd1d70f6..1d270419933 100644 --- a/homeassistant/components/sensor/gpsd.py +++ b/homeassistant/components/sensor/gpsd.py @@ -86,7 +86,6 @@ class GpsdSensor(Entity): """Return the name.""" return self._name - # pylint: disable=no-member @property def state(self): """Return the state of GPSD.""" diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index ca8c19bbc7a..1048c04d43d 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.isy994/ """ import logging -from typing import Callable # noqa +from typing import Callable from homeassistant.components.sensor import DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_WEATHER, diff --git a/homeassistant/components/sensor/kira.py b/homeassistant/components/sensor/kira.py index 74a1bd19d34..19566100f99 100644 --- a/homeassistant/components/sensor/kira.py +++ b/homeassistant/components/sensor/kira.py @@ -18,7 +18,6 @@ ICON = 'mdi:remote' CONF_SENSOR = 'sensor' -# pylint: disable=too-many-function-args def setup_platform(hass, config, add_devices, discovery_info=None): """Set up a Kira sensor.""" if discovery_info is not None: diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index ee9ab146c87..6ee3f7d16d0 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -68,7 +68,6 @@ class LastfmSensor(Entity): """Return the state of the sensor.""" return self._state - # pylint: disable=no-member def update(self): """Update device state.""" self._cover = self._user.get_image() diff --git a/homeassistant/components/sensor/loopenergy.py b/homeassistant/components/sensor/loopenergy.py index 09ed4ab3d49..d888a6c634d 100644 --- a/homeassistant/components/sensor/loopenergy.py +++ b/homeassistant/components/sensor/loopenergy.py @@ -63,7 +63,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elec_config = config.get(CONF_ELEC) gas_config = config.get(CONF_GAS, {}) - # pylint: disable=too-many-function-args controller = pyloopenergy.LoopEnergy( elec_config.get(CONF_ELEC_SERIAL), elec_config.get(CONF_ELEC_SECRET), diff --git a/homeassistant/components/sensor/mfi.py b/homeassistant/components/sensor/mfi.py index f6bec3284c3..ab6bd8270ce 100644 --- a/homeassistant/components/sensor/mfi.py +++ b/homeassistant/components/sensor/mfi.py @@ -49,7 +49,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up mFi sensors.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/sensor/pvoutput.py b/homeassistant/components/sensor/pvoutput.py index 26c3e27bba5..d4307d50228 100644 --- a/homeassistant/components/sensor/pvoutput.py +++ b/homeassistant/components/sensor/pvoutput.py @@ -64,7 +64,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([PvoutputSensor(rest, name)], True) -# pylint: disable=no-member class PvoutputSensor(Entity): """Representation of a PVOutput sensor.""" diff --git a/homeassistant/components/sensor/skybeacon.py b/homeassistant/components/sensor/skybeacon.py index 53cbaab19a5..2731587ed71 100644 --- a/homeassistant/components/sensor/skybeacon.py +++ b/homeassistant/components/sensor/skybeacon.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Skybeacon sensor.""" - # pylint: disable=unreachable name = config.get(CONF_NAME) mac = config.get(CONF_MAC) _LOGGER.debug("Setting up...") @@ -139,7 +138,7 @@ class Monitor(threading.Thread): def run(self): """Thread that keeps connection alive.""" - # pylint: disable=import-error, no-name-in-module, no-member + # pylint: disable=import-error import pygatt from pygatt.backends import Characteristic from pygatt.exceptions import ( diff --git a/homeassistant/components/sensor/sma.py b/homeassistant/components/sensor/sma.py index 3451789424b..2be46da0bdb 100644 --- a/homeassistant/components/sensor/sma.py +++ b/homeassistant/components/sensor/sma.py @@ -198,5 +198,4 @@ class SMAsensor(Entity): update = True self._state = new_state - return self.async_update_ha_state() if update else None \ - # pylint: disable=protected-access + return self.async_update_ha_state() if update else None diff --git a/homeassistant/components/sensor/steam_online.py b/homeassistant/components/sensor/steam_online.py index e22e1594b55..7521b74cd28 100644 --- a/homeassistant/components/sensor/steam_online.py +++ b/homeassistant/components/sensor/steam_online.py @@ -76,7 +76,6 @@ class SteamSensor(Entity): """Return the state of the sensor.""" return self._state - # pylint: disable=no-member def update(self): """Update device state.""" try: diff --git a/homeassistant/components/sensor/tado.py b/homeassistant/components/sensor/tado.py index ff8ad7fe849..737b3d08368 100644 --- a/homeassistant/components/sensor/tado.py +++ b/homeassistant/components/sensor/tado.py @@ -147,7 +147,6 @@ class TadoSensor(Entity): unit = TEMP_CELSIUS - # pylint: disable=R0912 if self.zone_variable == 'temperature': if 'sensorDataPoints' in data: sensor_data = data['sensorDataPoints'] diff --git a/homeassistant/components/sensor/ted5000.py b/homeassistant/components/sensor/ted5000.py index 55d520cf6ca..c2ef1d4c6b9 100644 --- a/homeassistant/components/sensor/ted5000.py +++ b/homeassistant/components/sensor/ted5000.py @@ -32,7 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Ted5000 sensor.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/sensor/wirelesstag.py b/homeassistant/components/sensor/wirelesstag.py index c93da3c791f..ad2115e9bd3 100755 --- a/homeassistant/components/sensor/wirelesstag.py +++ b/homeassistant/components/sensor/wirelesstag.py @@ -168,7 +168,7 @@ class WirelessTagSensor(WirelessTagBaseSensor): new_value = event.data.get('cap') elif self._sensor_type == SENSOR_LIGHT: new_value = event.data.get('lux') - except Exception as error: # pylint: disable=W0703 + except Exception as error: # pylint: disable=broad-except _LOGGER.info("Unable to update value of entity: \ %s error: %s event: %s", self, error, event) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 88c23771bd4..c7ff967723b 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -117,7 +117,7 @@ class YrSensor(Entity): return self._state @property - def should_poll(self): # pylint: disable=no-self-use + def should_poll(self): """No polling needed.""" return False diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index fe295d84d49..b2a913c2af8 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -5,8 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.zwave/ """ import logging -# Because we do not compile openzwave on CI -# pylint: disable=import-error from homeassistant.components.sensor import DOMAIN from homeassistant.components import zwave from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 9a35198628a..bab2abbad0d 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -135,7 +135,6 @@ async def async_setup(hass, config): class SwitchDevice(ToggleEntity): """Representation of a switch.""" - # pylint: disable=no-self-use @property def current_power_w(self): """Return the current power usage in W.""" diff --git a/homeassistant/components/switch/anel_pwrctrl.py b/homeassistant/components/switch/anel_pwrctrl.py index 30739676f17..4e62b711979 100644 --- a/homeassistant/components/switch/anel_pwrctrl.py +++ b/homeassistant/components/switch/anel_pwrctrl.py @@ -33,7 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up PwrCtrl devices/switches.""" host = config.get(CONF_HOST, None) diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index f57843cdaa0..7df8f0e1aa6 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -218,7 +218,6 @@ class FluxSwitch(SwitchDevice): else: sunset_time = sunset - # pylint: disable=no-member night_length = int(stop_time.timestamp() - sunset_time.timestamp()) seconds_from_sunset = int(now.timestamp() - diff --git a/homeassistant/components/switch/fritzdect.py b/homeassistant/components/switch/fritzdect.py index 58ad745a2d2..9968f631260 100644 --- a/homeassistant/components/switch/fritzdect.py +++ b/homeassistant/components/switch/fritzdect.py @@ -52,7 +52,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): fritz = FritzBox(host, username, password) try: fritz.login() - except Exception: # pylint: disable=W0703 + except Exception: # pylint: disable=broad-except _LOGGER.error("Login to Fritz!Box failed") return diff --git a/homeassistant/components/switch/gc100.py b/homeassistant/components/switch/gc100.py index 54c3b5e942a..34a29483d3c 100644 --- a/homeassistant/components/switch/gc100.py +++ b/homeassistant/components/switch/gc100.py @@ -39,7 +39,6 @@ class GC100Switch(ToggleEntity): def __init__(self, name, port_addr, gc100): """Initialize the GC100 switch.""" - # pylint: disable=no-member self._name = name or DEVICE_DEFAULT_NAME self._port_addr = port_addr self._gc100 = gc100 diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py index 3d29c53bd7c..2a7dee87747 100644 --- a/homeassistant/components/switch/isy994.py +++ b/homeassistant/components/switch/isy994.py @@ -5,12 +5,12 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.isy994/ """ import logging -from typing import Callable # noqa +from typing import Callable from homeassistant.components.switch import SwitchDevice, DOMAIN from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, ISYDevice) -from homeassistant.helpers.typing import ConfigType # noqa +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/mfi.py b/homeassistant/components/switch/mfi.py index c0dc72440d3..2c547fa210f 100644 --- a/homeassistant/components/switch/mfi.py +++ b/homeassistant/components/switch/mfi.py @@ -39,7 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """Set up mFi sensors.""" host = config.get(CONF_HOST) diff --git a/homeassistant/components/switch/rpi_rf.py b/homeassistant/components/switch/rpi_rf.py index 62c92ad2d96..03f11de21f7 100644 --- a/homeassistant/components/switch/rpi_rf.py +++ b/homeassistant/components/switch/rpi_rf.py @@ -44,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=import-error, no-member +# pylint: disable=no-member def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return switches controlled by a generic RF device via GPIO.""" import rpi_rf diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 569566bcbfb..c18ad492d40 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -33,7 +33,6 @@ WEMO_OFF = 0 WEMO_STANDBY = 8 -# pylint: disable=too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Set up discovered WeMo switches.""" import pywemo.discovery as discovery diff --git a/homeassistant/components/switch/zwave.py b/homeassistant/components/switch/zwave.py index 3b82d87d7e7..8a0a1683aa4 100644 --- a/homeassistant/components/switch/zwave.py +++ b/homeassistant/components/switch/zwave.py @@ -6,8 +6,6 @@ https://home-assistant.io/components/switch.zwave/ """ import logging import time -# Because we do not compile openzwave on CI -# pylint: disable=import-error from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.components import zwave from homeassistant.components.zwave import workaround, async_setup_platform # noqa # pylint: disable=unused-import diff --git a/homeassistant/components/vacuum/demo.py b/homeassistant/components/vacuum/demo.py index bd501167ffa..45fd8de2696 100644 --- a/homeassistant/components/vacuum/demo.py +++ b/homeassistant/components/vacuum/demo.py @@ -50,7 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DemoVacuum(VacuumDevice): """Representation of a demo vacuum.""" - # pylint: disable=no-self-use def __init__(self, name, supported_features): """Initialize the vacuum.""" self._name = name diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index ef3bb0f636b..8c2f110257f 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -210,7 +210,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class MqttVacuum(MqttAvailability, VacuumDevice): """Representation of a MQTT-controlled vacuum.""" - # pylint: disable=no-self-use def __init__( self, name, supported_features, qos, retain, command_topic, payload_turn_on, payload_turn_off, payload_return_to_base, diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index cbbf279bb8c..0ab5e7ce39a 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -53,7 +53,6 @@ VERA_COMPONENTS = [ ] -# pylint: disable=too-many-function-args def setup(hass, base_config): """Set up for Vera devices.""" import pyvera as veraApi diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index c36c960c4fc..a43999f2276 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -46,7 +46,6 @@ def async_setup(hass, config): return True -# pylint: disable=no-member, no-self-use class WeatherEntity(Entity): """ABC for weather data.""" diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index e16e5524f95..aacef4547b7 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -228,7 +228,6 @@ class WebsocketAPIView(HomeAssistantView): async def get(self, request): """Handle an incoming websocket connection.""" - # pylint: disable=no-self-use return await ActiveConnection(request.app['hass'], request).handle() diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py index d38a42e2cbf..e8c7db5efe1 100644 --- a/homeassistant/components/wemo.py +++ b/homeassistant/components/wemo.py @@ -44,7 +44,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=too-many-function-args def setup(hass, config): """Set up for WeMo devices.""" import pywemo diff --git a/homeassistant/components/wirelesstag.py b/homeassistant/components/wirelesstag.py index 9fabcb1cd5a..0f8f47f5100 100644 --- a/homeassistant/components/wirelesstag.py +++ b/homeassistant/components/wirelesstag.py @@ -146,7 +146,7 @@ class WirelessTagPlatform: self.hass, SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type), event) - except Exception as ex: # pylint: disable=W0703 + except Exception as ex: # pylint: disable=broad-except _LOGGER.error("Unable to handle binary event:\ %s error: %s", str(event), str(ex)) diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder.py index 86531401774..471c1c6e82c 100644 --- a/homeassistant/components/zoneminder.py +++ b/homeassistant/components/zoneminder.py @@ -67,7 +67,6 @@ def setup(hass, config): return login() -# pylint: disable=no-member def login(): """Login to the ZoneMinder API.""" _LOGGER.debug("Attempting to login to ZoneMinder") @@ -118,13 +117,11 @@ def _zm_request(method, api_url, data=None): 'decode "%s"', req.text) -# pylint: disable=no-member def get_state(api_url): """Get a state from the ZoneMinder API service.""" return _zm_request('get', api_url) -# pylint: disable=no-member def change_state(api_url, post_data): """Update a state using the Zoneminder API.""" return _zm_request('post', api_url, data=post_data) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index a8ba5e4a6d3..e540259edd5 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -218,7 +218,6 @@ async def async_setup_platform(hass, config, async_add_devices, return True -# pylint: disable=R0914 async def async_setup(hass, config): """Set up Z-Wave. diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 3e503e4d9a4..0228e64cf6e 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -345,7 +345,6 @@ DISC_SPECIFIC_DEVICE_CLASS = "specific_device_class" DISC_TYPE = "type" DISC_VALUES = "values" -# noqa # https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Alarm.cpp#L49 # See also: # https://github.com/OpenZWave/open-zwave/blob/67f180eb565f0054f517ff395c71ecd706f6a837/cpp/src/command_classes/Alarm.cpp#L275 diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 91ec5051552..54cd569aceb 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -6,7 +6,7 @@ from typing import Any, Iterable, Tuple, Sequence, Dict from homeassistant.const import CONF_PLATFORM # Typing Imports and TypeAlias -# pylint: disable=using-constant-test,unused-import,wrong-import-order +# pylint: disable=using-constant-test,unused-import if False: from logging import Logger # NOQA @@ -14,7 +14,6 @@ if False: ConfigType = Dict[str, Any] -# pylint: disable=invalid-sequence-index def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any, Any]]: """Break a component config into different platforms. diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index bb34942ad79..5ee2cd56081 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -128,7 +128,6 @@ async def async_aiohttp_proxy_stream(hass, request, stream, content_type, @callback -# pylint: disable=invalid-name def _async_register_clientsession_shutdown(hass, clientsession): """Register ClientSession close on Home Assistant shutdown. diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 85050b5736f..7dc5d2524ec 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -59,7 +59,6 @@ def async_generate_entity_id(entity_id_format: str, name: Optional[str], class Entity(object): """An abstract class for Home Assistant entities.""" - # pylint: disable=no-self-use # SAFE TO OVERWRITE # The properties and methods here are safe to overwrite when inheriting # this class. These may be used to customize the behavior of the entity. @@ -365,7 +364,6 @@ class Entity(object): class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" - # pylint: disable=no-self-use @property def state(self) -> str: """Return the state.""" diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 4a2cd5fa50c..04d9cc450ba 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -37,8 +37,6 @@ DISABLED_USER = 'user' class RegistryEntry: """Entity Registry Entry.""" - # pylint: disable=no-member - entity_id = attr.ib(type=str) unique_id = attr.ib(type=str) platform = attr.ib(type=str) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index d69a556b0cc..712b48da0d7 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -133,7 +133,6 @@ def async_track_same_state(hass, period, action, async_check_same_func, """Clear all unsub listener.""" nonlocal async_remove_state_for_cancel, async_remove_state_for_listener - # pylint: disable=not-callable if async_remove_state_for_listener is not None: async_remove_state_for_listener() async_remove_state_for_listener = None diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 5a33bd58641..b3e5f417618 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -31,7 +31,6 @@ _LOGGER = logging.getLogger(__name__) class APIStatus(enum.Enum): """Representation of an API status.""" - # pylint: disable=no-init, invalid-name OK = "ok" INVALID_PASSWORD = "invalid_password" CANNOT_CONNECT = "cannot_connect" diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 3a1ffa82d47..69b1bf21c08 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -267,7 +267,7 @@ def dump_dict(layer, indent_count=3, listi=False, **kwargs): print(' ', indent_str, i) -CheckConfigError = namedtuple( # pylint: disable=invalid-name +CheckConfigError = namedtuple( 'CheckConfigError', "message domain config") @@ -378,7 +378,6 @@ def check_ha_config_file(hass): # Validate platform specific schema if hasattr(platform, 'PLATFORM_SCHEMA'): - # pylint: disable=no-member try: p_validated = platform.PLATFORM_SCHEMA(p_validated) except vol.Invalid as ex: diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index a8a84c6c880..bbf0f7e11e2 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -120,7 +120,6 @@ def get_random_string(length=10): class OrderedEnum(enum.Enum): """Taken from Python 3.4.0 docs.""" - # pylint: disable=no-init def __ge__(self, other): """Return the greater than element.""" if self.__class__ is other.__class__: diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 5676a1d0844..b3aa370da2e 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -107,7 +107,6 @@ def run_coroutine_threadsafe(coro, loop): def callback(): """Handle the call to the coroutine.""" try: - # pylint: disable=deprecated-method _chain_future(ensure_future(coro, loop=loop), future) # pylint: disable=broad-except except Exception as exc: @@ -136,7 +135,6 @@ def fire_coroutine_threadsafe(coro, loop): def callback(): """Handle the firing of a coroutine.""" - # pylint: disable=deprecated-method ensure_future(coro, loop=loop) loop.call_soon_threadsafe(callback) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 32e9df70a03..d2138f4293c 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -173,7 +173,7 @@ def color_name_to_rgb(color_name): return hex_value -# pylint: disable=invalid-name, invalid-sequence-index +# pylint: disable=invalid-name def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float]: """Convert from RGB color to XY color.""" return color_RGB_to_xy_brightness(iR, iG, iB)[:2] @@ -182,7 +182,7 @@ def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float]: # Taken from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy # License: Code is given as is. Use at your own risk and discretion. -# pylint: disable=invalid-name, invalid-sequence-index +# pylint: disable=invalid-name def color_RGB_to_xy_brightness( iR: int, iG: int, iB: int) -> Tuple[float, float, int]: """Convert from RGB color to XY color.""" @@ -224,7 +224,6 @@ def color_xy_to_RGB(vX: float, vY: float) -> Tuple[int, int, int]: # Converted to Python from Obj-C, original source from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy -# pylint: disable=invalid-sequence-index def color_xy_brightness_to_RGB(vX: float, vY: float, ibrightness: int) -> Tuple[int, int, int]: """Convert from XYZ to RGB.""" @@ -265,7 +264,6 @@ def color_xy_brightness_to_RGB(vX: float, vY: float, return (ir, ig, ib) -# pylint: disable=invalid-sequence-index def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> Tuple[int, int, int]: """Convert a hsb into its rgb representation.""" if fS == 0: @@ -307,7 +305,6 @@ def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> Tuple[int, int, int]: return (r, g, b) -# pylint: disable=invalid-sequence-index def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[float, float, float]: """Convert an rgb color to its hsv representation. @@ -319,13 +316,11 @@ def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[float, float, float]: return round(fHSV[0]*360, 3), round(fHSV[1]*100, 3), round(fHSV[2]*100, 3) -# pylint: disable=invalid-sequence-index def color_RGB_to_hs(iR: int, iG: int, iB: int) -> Tuple[float, float]: """Convert an rgb color to its hs representation.""" return color_RGB_to_hsv(iR, iG, iB)[:2] -# pylint: disable=invalid-sequence-index def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]: """Convert an hsv color into its rgb representation. @@ -337,26 +332,22 @@ def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]: return (int(fRGB[0]*255), int(fRGB[1]*255), int(fRGB[2]*255)) -# pylint: disable=invalid-sequence-index def color_hs_to_RGB(iH: float, iS: float) -> Tuple[int, int, int]: """Convert an hsv color into its rgb representation.""" return color_hsv_to_RGB(iH, iS, 100) -# pylint: disable=invalid-sequence-index def color_xy_to_hs(vX: float, vY: float) -> Tuple[float, float]: """Convert an xy color to its hs representation.""" h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY)) return (h, s) -# pylint: disable=invalid-sequence-index def color_hs_to_xy(iH: float, iS: float) -> Tuple[float, float]: """Convert an hs color to its xy representation.""" return color_RGB_to_xy(*color_hs_to_RGB(iH, iS)) -# pylint: disable=invalid-sequence-index def _match_max_scale(input_colors: Tuple[int, ...], output_colors: Tuple[int, ...]) -> Tuple[int, ...]: """Match the maximum value of the output to the input.""" diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index cd440783cc3..37b917baa2e 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -184,7 +184,6 @@ def get_age(date: dt.datetime) -> str: elif number > 1: return "%d %ss" % (number, unit) - # pylint: disable=invalid-sequence-index def q_n_r(first: int, second: int) -> Tuple[int, int]: """Return quotient and remaining.""" return first // second, first % second diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index dae8ed17dc9..e390b537d34 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -82,7 +82,7 @@ def elevation(latitude, longitude): # Author: https://github.com/maurycyp # Source: https://github.com/maurycyp/vincenty # License: https://github.com/maurycyp/vincenty/blob/master/LICENSE -# pylint: disable=invalid-name, unused-variable, invalid-sequence-index +# pylint: disable=invalid-name def vincenty(point1: Tuple[float, float], point2: Tuple[float, float], miles: bool = False) -> Optional[float]: """ diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 66d673987a3..0e7befd5e9e 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -13,7 +13,7 @@ except ImportError: keyring = None try: - import credstash # pylint: disable=import-error, no-member + import credstash except ImportError: credstash = None @@ -246,7 +246,6 @@ def _load_secret_yaml(secret_path: str) -> Dict: return secrets -# pylint: disable=protected-access def _secret_yaml(loader: SafeLineLoader, node: yaml.nodes.Node): """Load secrets and embed it into the configuration YAML.""" diff --git a/script/lazytox.py b/script/lazytox.py index 19af5560dfb..f0388a0fdcb 100755 --- a/script/lazytox.py +++ b/script/lazytox.py @@ -39,7 +39,6 @@ def printc(the_color, *args): def validate_requirements_ok(): """Validate requirements, returns True of ok.""" - # pylint: disable=E0402 from gen_requirements_all import main as req_main return req_main(True) == 0 @@ -70,7 +69,6 @@ async def async_exec(*args, display=False): 'stderr': asyncio.subprocess.STDOUT} if display: kwargs['stderr'] = asyncio.subprocess.PIPE - # pylint: disable=E1120 proc = await asyncio.create_subprocess_exec(*args, **kwargs) except FileNotFoundError as err: printc(FAIL, "Could not execute {}. Did you install test requirements?" From 9dd2c36de4563fe6b7929611b7ab523d04516006 Mon Sep 17 00:00:00 2001 From: Luc Touraille Date: Mon, 25 Jun 2018 19:05:33 +0200 Subject: [PATCH 073/128] Update aiofreepybox to fix HTTPS connection issues (#15104) The previous version of aiofreepybox was not working with custom domain names, which uses a Let's Encrypt certificates. Also, it was not working with the default domain name when connecting to Freebox v6. This should be fixed in aiofreepybox 0.0.4. See https://github.com/stilllman/freepybox/pull/1, https://github.com/stilllman/freepybox/pull/3 and https://github.com/stilllman/freepybox/issues/2 for more info. --- homeassistant/components/device_tracker/freebox.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/freebox.py b/homeassistant/components/device_tracker/freebox.py index 67957ca99b9..b278c421925 100644 --- a/homeassistant/components/device_tracker/freebox.py +++ b/homeassistant/components/device_tracker/freebox.py @@ -22,7 +22,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.const import ( CONF_HOST, CONF_PORT) -REQUIREMENTS = ['aiofreepybox==0.0.3'] +REQUIREMENTS = ['aiofreepybox==0.0.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 78fda284b17..985bd8a1d24 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -84,7 +84,7 @@ aioautomatic==0.6.5 aiodns==1.1.1 # homeassistant.components.device_tracker.freebox -aiofreepybox==0.0.3 +aiofreepybox==0.0.4 # homeassistant.components.camera.yi aioftp==0.10.1 From b2d37ccef673178df6bdd72e827d03719c2348a9 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 25 Jun 2018 19:06:12 +0200 Subject: [PATCH 074/128] Fix mysensors climate supported features (#15110) --- homeassistant/components/climate/mysensors.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 37ae29fdf81..a2043c2434b 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -26,9 +26,8 @@ DICT_MYS_TO_HA = { 'Off': STATE_OFF, } -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE | - SUPPORT_OPERATION_MODE) +FAN_LIST = ['Auto', 'Min', 'Normal', 'Max'] +OPERATION_LIST = [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT] async def async_setup_platform( @@ -45,7 +44,18 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_FLAGS + features = SUPPORT_OPERATION_MODE + set_req = self.gateway.const.SetReq + if set_req.V_HVAC_SPEED in self._values: + features = features | SUPPORT_FAN_MODE + if (set_req.V_HVAC_SETPOINT_COOL in self._values and + set_req.V_HVAC_SETPOINT_HEAT in self._values): + features = ( + features | SUPPORT_TARGET_TEMPERATURE_HIGH | + SUPPORT_TARGET_TEMPERATURE_LOW) + else: + features = features | SUPPORT_TARGET_TEMPERATURE + return features @property def assumed_state(self): @@ -103,7 +113,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): @property def operation_list(self): """List of available operation modes.""" - return [STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT] + return OPERATION_LIST @property def current_fan_mode(self): @@ -113,7 +123,7 @@ class MySensorsHVAC(mysensors.device.MySensorsEntity, ClimateDevice): @property def fan_list(self): """List of available fan modes.""" - return ['Auto', 'Min', 'Normal', 'Max'] + return FAN_LIST async def async_set_temperature(self, **kwargs): """Set new target temperature.""" From e681a7929c8b51abb0f6a6435f0bc437fdc37548 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 25 Jun 2018 10:13:41 -0700 Subject: [PATCH 075/128] Skip nest security state sensor if no Nest Cam exists (#15112) --- homeassistant/components/sensor/nest.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 7afd3b762b3..d2e1501ad7e 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -24,10 +24,14 @@ PROTECT_SENSOR_TYPES = ['co_status', # color_status: "gray", "green", "yellow", "red" 'color_status'] -STRUCTURE_SENSOR_TYPES = ['eta', 'security_state'] +STRUCTURE_SENSOR_TYPES = ['eta'] + +# security_state is structure level sensor, but only meaningful when +# Nest Cam exist +STRUCTURE_CAMERA_SENSOR_TYPES = ['security_state'] _VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \ - + STRUCTURE_SENSOR_TYPES + + STRUCTURE_SENSOR_TYPES + STRUCTURE_CAMERA_SENSOR_TYPES SENSOR_UNITS = {'humidity': '%'} @@ -105,6 +109,14 @@ async def async_setup_entry(hass, entry, async_add_devices): for variable in conditions if variable in PROTECT_SENSOR_TYPES] + structures_has_camera = {} + for structure, device in nest.cameras(): + structures_has_camera[structure] = True + for structure in structures_has_camera: + all_sensors += [NestBasicSensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_CAMERA_SENSOR_TYPES] + return all_sensors async_add_devices(await hass.async_add_job(get_sensors), True) From c8458fd7c5364d71e849c505ee27f7cc335cbed8 Mon Sep 17 00:00:00 2001 From: Sriram Vaidyanathan Date: Mon, 25 Jun 2018 22:44:36 +0530 Subject: [PATCH 076/128] Update xiaomi.py (#15136) * Update xiaomi.py Minor logic fix for Xiaofang cameras. * Removed whitespace * Removed whitespace --- homeassistant/components/camera/xiaomi.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/camera/xiaomi.py b/homeassistant/components/camera/xiaomi.py index c18a3649e7b..f0e66dbd20e 100644 --- a/homeassistant/components/camera/xiaomi.py +++ b/homeassistant/components/camera/xiaomi.py @@ -104,20 +104,16 @@ class XiaomiCamera(Camera): dirs = [d for d in ftp.nlst() if '.' not in d] if not dirs: - if self._model == MODEL_YI: - _LOGGER.warning("There don't appear to be any uploaded videos") - return False - elif self._model == MODEL_XIAOFANG: - _LOGGER.warning("There don't appear to be any folders") - return False - - first_dir = dirs[-1] - try: - ftp.cwd(first_dir) - except error_perm as exc: - _LOGGER.error('Unable to find path: %s - %s', first_dir, exc) - return False + _LOGGER.warning("There don't appear to be any folders") + return False + first_dir = dirs[-1] + try: + ftp.cwd(first_dir) + except error_perm as exc: + _LOGGER.error('Unable to find path: %s - %s', first_dir, exc) + return False + if self._model == MODEL_XIAOFANG: dirs = [d for d in ftp.nlst() if '.' not in d] if not dirs: _LOGGER.warning("There don't appear to be any uploaded videos") From 46ea28a4f81b121967a4041a5baa5d34e0827ac7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Jun 2018 15:59:05 -0400 Subject: [PATCH 077/128] Fix cast config (#15143) --- homeassistant/components/media_player/cast.py | 11 +++- tests/components/media_player/test_cast.py | 55 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index be7b635f863..4e24d5f2f71 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -4,6 +4,7 @@ Provide functionality to interact with Cast devices on the network. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.cast/ """ +import asyncio import logging import threading from typing import Optional, Tuple @@ -199,9 +200,13 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async def async_setup_entry(hass, config_entry, async_add_devices): """Set up Cast from a config entry.""" - await _async_setup_platform( - hass, hass.data[CAST_DOMAIN].get('media_player', {}), - async_add_devices, None) + config = hass.data[CAST_DOMAIN].get('media_player', {}) + if not isinstance(config, list): + config = [config] + + await asyncio.wait([ + _async_setup_platform(hass, cfg, async_add_devices, None) + for cfg in config]) async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType, diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 41cf6749b71..47be39c68e5 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -17,6 +17,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ from homeassistant.components.media_player import cast from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry, mock_coro + @pytest.fixture(autouse=True) def cast_mock(): @@ -359,3 +361,56 @@ async def test_disconnect_on_stop(hass: HomeAssistantType): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() assert chromecast.disconnect.call_count == 1 + + +async def test_entry_setup_no_config(hass: HomeAssistantType): + """Test setting up entry with no config..""" + await async_setup_component(hass, 'cast', {}) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == {} + + +async def test_entry_setup_single_config(hass: HomeAssistantType): + """Test setting up entry and having a single config option.""" + await async_setup_component(hass, 'cast', { + 'cast': { + 'media_player': { + 'host': 'bla' + } + } + }) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'} + + +async def test_entry_setup_list_config(hass: HomeAssistantType): + """Test setting up entry and having multiple config options.""" + await async_setup_component(hass, 'cast', { + 'cast': { + 'media_player': [ + {'host': 'bla'}, + {'host': 'blu'}, + ] + } + }) + + with patch( + 'homeassistant.components.media_player.cast._async_setup_platform', + return_value=mock_coro()) as mock_setup: + await cast.async_setup_entry(hass, MockConfigEntry(), None) + + assert len(mock_setup.mock_calls) == 2 + assert mock_setup.mock_calls[0][1][1] == {'host': 'bla'} + assert mock_setup.mock_calls[1][1][1] == {'host': 'blu'} From 15507df407d9e3db5fff06bc7dbbc54e6af8e10b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Jun 2018 16:04:17 -0400 Subject: [PATCH 078/128] Bump frontend to 20180625.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 3d2231ab43b..54a77af5cfb 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180622.1'] +REQUIREMENTS = ['home-assistant-frontend==20180625.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 985bd8a1d24..6b9da71bd1f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180622.1 +home-assistant-frontend==20180625.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04952b75b81..e9226b30498 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==20180622.1 +home-assistant-frontend==20180625.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 6e4fb7a937fa7f64bc233183c6ee1697a46f5a81 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 25 Jun 2018 13:06:00 -0700 Subject: [PATCH 079/128] Prevent Nest component setup crash due insufficient permission. (#14966) * Prevent Nest component setup crash due insufficient permission. * Trigger CI * Better error handle and address code review comments * Lint * Tiny wording adjust * Notify user if async_setup_entry failed * Return False if exception occurred in NestDevice.initialize --- homeassistant/components/nest/__init__.py | 85 +++++++++++++---------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index f9507b6ec7b..58fa1953ef0 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -123,7 +123,8 @@ async def async_setup_entry(hass, entry): _LOGGER.debug("proceeding with setup") conf = hass.data.get(DATA_NEST_CONFIG, {}) hass.data[DATA_NEST] = NestDevice(hass, conf, nest) - await hass.async_add_job(hass.data[DATA_NEST].initialize) + if not await hass.async_add_job(hass.data[DATA_NEST].initialize): + return False for component in 'climate', 'camera', 'sensor', 'binary_sensor': hass.async_add_job(hass.config_entries.async_forward_entry_setup( @@ -193,63 +194,73 @@ class NestDevice(object): def initialize(self): """Initialize Nest.""" - if self.local_structure is None: - self.local_structure = [s.name for s in self.nest.structures] + from nest.nest import AuthorizationError, APIError + try: + # Do not optimize next statement, it is here for initialize + # persistence Nest API connection. + structure_names = [s.name for s in self.nest.structures] + if self.local_structure is None: + self.local_structure = structure_names + + except (AuthorizationError, APIError, socket.error) as err: + _LOGGER.error( + "Connection error while access Nest web service: %s", err) + return False + return True def structures(self): """Generate a list of structures.""" + from nest.nest import AuthorizationError, APIError try: for structure in self.nest.structures: - if structure.name in self.local_structure: - yield structure - else: + if structure.name not in self.local_structure: _LOGGER.debug("Ignoring structure %s, not in %s", structure.name, self.local_structure) - except socket.error: + continue + yield structure + + except (AuthorizationError, APIError, socket.error) as err: _LOGGER.error( - "Connection error logging into the nest web service.") + "Connection error while access Nest web service: %s", err) def thermostats(self): - """Generate a list of thermostats and their location.""" - try: - for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.thermostats: - yield (structure, device) - 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.") + """Generate a list of thermostats.""" + return self._devices('thermostats') def smoke_co_alarms(self): """Generate a list of smoke co alarms.""" - try: - for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.smoke_co_alarms: - yield (structure, device) - 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.") + return self._devices('smoke_co_alarms') def cameras(self): """Generate a list of cameras.""" + return self._devices('cameras') + + def _devices(self, device_type): + """Generate a list of Nest devices.""" + from nest.nest import AuthorizationError, APIError try: for structure in self.nest.structures: - if structure.name in self.local_structure: - for device in structure.cameras: - yield (structure, device) - else: + if structure.name not in self.local_structure: _LOGGER.debug("Ignoring structure %s, not in %s", structure.name, self.local_structure) - except socket.error: + continue + + for device in getattr(structure, device_type, []): + try: + # Do not optimize next statement, + # it is here for verify Nest API permission. + device.name_long + except KeyError: + _LOGGER.warning("Cannot retrieve device name for [%s]" + ", please check your Nest developer " + "account permission settings.", + device.serial) + continue + yield (structure, device) + + except (AuthorizationError, APIError, socket.error) as err: _LOGGER.error( - "Connection error logging into the nest web service.") + "Connection error while access Nest web service: %s", err) class NestSensorDevice(Entity): From c79c94550faf75d8833b11bfe655914ae5a2ac80 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Jun 2018 17:21:38 -0400 Subject: [PATCH 080/128] Return None to indicate no config found (#15147) * Return None to indicate no config found * Fix tests --- homeassistant/config_entries.py | 4 ++++ homeassistant/helpers/storage.py | 2 +- tests/helpers/test_storage.py | 2 +- tests/test_config_entries.py | 10 ++++++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 13cb7de62ef..be67ebd9cc3 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -323,6 +323,10 @@ class ConfigEntries: old_conf_migrate_func=_old_conf_migrator ) + if config is None: + self._entries = [] + return + self._entries = [ConfigEntry(**entry) for entry in config['entries']] async def async_forward_entry_setup(self, entry, component): diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 4b0c576f129..18c3ddf7fcd 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -72,7 +72,7 @@ class Store: json.load_json, self.path, None) if data is None: - return {} + return None if data['version'] == self.version: return data['data'] diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 289d07edab2..04de920b036 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -52,7 +52,7 @@ async def test_loading_non_existing(hass, store): """Test we can save and load data.""" with patch('homeassistant.util.json.open', side_effect=FileNotFoundError): data = await store.async_load() - assert data == {} + assert data is None async def test_saving_with_delay(hass, store, mock_save): diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index fc0a549f1ae..b65e0dd62e7 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -309,3 +309,13 @@ async def test_discovery_notification_not_created(hass): await hass.async_block_till_done() state = hass.states.get('persistent_notification.config_entry_discovery') assert state is None + + +async def test_loading_default_config(hass): + """Test loading the default config.""" + manager = config_entries.ConfigEntries(hass, {}) + + with patch('homeassistant.util.json.open', side_effect=FileNotFoundError): + await manager.async_load() + + assert len(manager.async_entries()) == 0 From 0094fd5c34c7292491bba06188c5f8f1860a03bb Mon Sep 17 00:00:00 2001 From: Matt LeBrun Date: Tue, 26 Jun 2018 10:22:10 -0400 Subject: [PATCH 081/128] Add channel changing support to SamsungTV component (#14451) Add channel changing support to SamsungTV component --- .../components/media_player/samsungtv.py | 26 +++++++- .../components/media_player/test_samsungtv.py | 63 ++++++++++++++++++- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 15a2b41795e..c3de341d607 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -4,6 +4,7 @@ Support for interface with an Samsung TV. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.samsungtv/ """ +import asyncio import logging import socket from datetime import timedelta @@ -15,8 +16,9 @@ import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_TURN_ON) + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_PLAY, + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY_MEDIA, + MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_CHANNEL) from homeassistant.const import ( CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT, CONF_MAC) @@ -32,12 +34,13 @@ CONF_TIMEOUT = 'timeout' DEFAULT_NAME = 'Samsung TV Remote' DEFAULT_PORT = 55000 DEFAULT_TIMEOUT = 0 +KEY_PRESS_TIMEOUT = 1.2 KNOWN_DEVICES_KEY = 'samsungtv_known_devices' SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | SUPPORT_PLAY + SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -256,6 +259,23 @@ class SamsungTVDevice(MediaPlayerDevice): """Send the previous track command.""" self.send_key('KEY_REWIND') + async def async_play_media(self, media_type, media_id, **kwargs): + """Support changing a channel.""" + if media_type != MEDIA_TYPE_CHANNEL: + _LOGGER.error('Unsupported media type') + return + + # media_id should only be a channel number + try: + cv.positive_int(media_id) + except vol.Invalid: + _LOGGER.error('Media ID must be positive integer') + return + + for digit in media_id: + await self.hass.async_add_job(self.send_key, 'KEY_' + digit) + await asyncio.sleep(KEY_PRESS_TIMEOUT, self.hass.loop) + def turn_on(self): """Turn the media player on.""" if self._mac: diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py index b5baf8b078b..349067f7cd3 100644 --- a/tests/components/media_player/test_samsungtv.py +++ b/tests/components/media_player/test_samsungtv.py @@ -1,11 +1,16 @@ """Tests for samsungtv Components.""" +import asyncio import unittest +from unittest.mock import call, patch, MagicMock from subprocess import CalledProcessError from asynctest import mock +import pytest + import tests.common -from homeassistant.components.media_player import SUPPORT_TURN_ON +from homeassistant.components.media_player import SUPPORT_TURN_ON, \ + MEDIA_TYPE_CHANNEL, MEDIA_TYPE_URL from homeassistant.components.media_player.samsungtv import setup_platform, \ CONF_TIMEOUT, SamsungTVDevice, SUPPORT_SAMSUNGTV from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_ON, \ @@ -301,3 +306,59 @@ class TestSamsungTv(unittest.TestCase): self.device._mac = "fake" self.device.turn_on() self.device._wol.send_magic_packet.assert_called_once_with("fake") + + +@pytest.fixture +def samsung_mock(): + """Mock samsungctl.""" + with patch.dict('sys.modules', { + 'samsungctl': MagicMock(), + }): + yield + + +async def test_play_media(hass, samsung_mock): + """Test for play_media.""" + asyncio_sleep = asyncio.sleep + sleeps = [] + + async def sleep(duration, loop): + sleeps.append(duration) + await asyncio_sleep(0, loop=loop) + + with patch('asyncio.sleep', new=sleep): + device = SamsungTVDevice(**WORKING_CONFIG) + device.hass = hass + + device.send_key = mock.Mock() + await device.async_play_media(MEDIA_TYPE_CHANNEL, "576") + + exp = [call("KEY_5"), call("KEY_7"), call("KEY_6")] + assert device.send_key.call_args_list == exp + assert len(sleeps) == 3 + + +async def test_play_media_invalid_type(hass, samsung_mock): + """Test for play_media with invalid media type.""" + url = "https://example.com" + device = SamsungTVDevice(**WORKING_CONFIG) + device.send_key = mock.Mock() + await device.async_play_media(MEDIA_TYPE_URL, url) + assert device.send_key.call_count == 0 + + +async def test_play_media_channel_as_string(hass, samsung_mock): + """Test for play_media with invalid channel as string.""" + url = "https://example.com" + device = SamsungTVDevice(**WORKING_CONFIG) + device.send_key = mock.Mock() + await device.async_play_media(MEDIA_TYPE_CHANNEL, url) + assert device.send_key.call_count == 0 + + +async def test_play_media_channel_as_non_positive(hass, samsung_mock): + """Test for play_media with invalid channel as non positive integer.""" + device = SamsungTVDevice(**WORKING_CONFIG) + device.send_key = mock.Mock() + await device.async_play_media(MEDIA_TYPE_CHANNEL, "-4") + assert device.send_key.call_count == 0 From 3921dc77a6e36aec2148fb28d06de77fdfd2a184 Mon Sep 17 00:00:00 2001 From: Robert Kiss Date: Tue, 26 Jun 2018 17:44:08 +0200 Subject: [PATCH 082/128] Add SSL peer certificate support to HTTP server (#15043) * adding SSL peer certificate support to HTTP server * remove unnecessary exception block --- homeassistant/components/emulated_hue/__init__.py | 1 + homeassistant/components/http/__init__.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index fd7f7147fdb..708b3db83cd 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -91,6 +91,7 @@ def setup(hass, yaml_config): server_port=config.listen_port, api_password=None, ssl_certificate=None, + ssl_peer_certificate=None, ssl_key=None, cors_origins=None, use_x_forwarded_for=False, diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 17906157a6e..d8c877e83a2 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -40,6 +40,7 @@ CONF_SERVER_HOST = 'server_host' CONF_SERVER_PORT = 'server_port' CONF_BASE_URL = 'base_url' CONF_SSL_CERTIFICATE = 'ssl_certificate' +CONF_SSL_PEER_CERTIFICATE = 'ssl_peer_certificate' CONF_SSL_KEY = 'ssl_key' CONF_CORS_ORIGINS = 'cors_allowed_origins' CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' @@ -80,6 +81,7 @@ HTTP_SCHEMA = vol.Schema({ vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, vol.Optional(CONF_BASE_URL): cv.string, vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, + vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_KEY): cv.isfile, vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.All(cv.ensure_list, [cv.string]), @@ -108,6 +110,7 @@ async def async_setup(hass, config): server_host = conf[CONF_SERVER_HOST] server_port = conf[CONF_SERVER_PORT] ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) + ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE) ssl_key = conf.get(CONF_SSL_KEY) cors_origins = conf[CONF_CORS_ORIGINS] use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR] @@ -125,6 +128,7 @@ async def async_setup(hass, config): server_port=server_port, api_password=api_password, ssl_certificate=ssl_certificate, + ssl_peer_certificate=ssl_peer_certificate, ssl_key=ssl_key, cors_origins=cors_origins, use_x_forwarded_for=use_x_forwarded_for, @@ -166,7 +170,8 @@ async def async_setup(hass, config): class HomeAssistantHTTP(object): """HTTP server for Home Assistant.""" - def __init__(self, hass, api_password, ssl_certificate, + def __init__(self, hass, api_password, + ssl_certificate, ssl_peer_certificate, ssl_key, server_host, server_port, cors_origins, use_x_forwarded_for, trusted_networks, login_threshold, is_ban_enabled): @@ -190,6 +195,7 @@ class HomeAssistantHTTP(object): self.hass = hass self.api_password = api_password self.ssl_certificate = ssl_certificate + self.ssl_peer_certificate = ssl_peer_certificate self.ssl_key = ssl_key self.server_host = server_host self.server_port = server_port @@ -287,8 +293,12 @@ class HomeAssistantHTTP(object): except OSError as error: _LOGGER.error("Could not read SSL certificate from %s: %s", self.ssl_certificate, error) - context = None return + + if self.ssl_peer_certificate: + context.verify_mode = ssl.CERT_REQUIRED + context.load_verify_locations(cafile=self.ssl_peer_certificate) + else: context = None From 15af6b1ad905cd0d279665c6bd5178efad4823a8 Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Tue, 26 Jun 2018 14:23:57 -0500 Subject: [PATCH 083/128] Address inconsistent behavior on flux_led component (#14713) * Address inconsistent behavior between different controllers. Correct issue with comparison that was preventing white value slider from being shown. * Add white mode for Flux LED * Call _bulb.turnOn() after bulb properties have been set to prevent immediate on action * Only use existing brightness if rgb is None to prevent unexpected recalculation of passed rgb values. * Remove blank line * Undo change so current brightness is used in all cases. --- homeassistant/components/light/flux_led.py | 36 +++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index fc85e05238f..b9db9d4f99b 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -33,6 +33,10 @@ SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | MODE_RGB = 'rgb' MODE_RGBW = 'rgbw' +# This mode enables white value to be controlled by brightness. +# RGB value is ignored when this mode is specified. +MODE_WHITE = 'w' + # List of supported effects which aren't already declared in LIGHT EFFECT_RED_FADE = 'red_fade' EFFECT_GREEN_FADE = 'green_fade' @@ -84,7 +88,7 @@ FLUX_EFFECT_LIST = [ DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, vol.Optional(ATTR_MODE, default=MODE_RGBW): - vol.All(cv.string, vol.In([MODE_RGBW, MODE_RGB])), + vol.All(cv.string, vol.In([MODE_RGBW, MODE_RGB, MODE_WHITE])), vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(['ledenet'])), }) @@ -181,6 +185,9 @@ class FluxLight(Light): @property def brightness(self): """Return the brightness of this light between 0..255.""" + if self._mode == MODE_WHITE: + return self.white_value + return self._bulb.brightness @property @@ -191,9 +198,12 @@ class FluxLight(Light): @property def supported_features(self): """Flag supported features.""" - if self._mode is MODE_RGBW: + if self._mode == MODE_RGBW: return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE + if self._mode == MODE_WHITE: + return SUPPORT_BRIGHTNESS + return SUPPORT_FLUX_LED @property @@ -208,9 +218,6 @@ class FluxLight(Light): def turn_on(self, **kwargs): """Turn the specified or all lights on.""" - if not self.is_on: - self._bulb.turnOn() - hs_color = kwargs.get(ATTR_HS_COLOR) if hs_color: @@ -247,10 +254,23 @@ class FluxLight(Light): if rgb is None: rgb = self._bulb.getRgb() - self._bulb.setRgb(*tuple(rgb), brightness=brightness) + if white is None and self._mode == MODE_RGBW: + white = self.white_value - if white is not None: - self._bulb.setWarmWhite255(white) + # handle W only mode (use brightness instead of white value) + if self._mode == MODE_WHITE: + self._bulb.setRgbw(0, 0, 0, w=brightness) + + # handle RGBW mode + elif self._mode == MODE_RGBW: + self._bulb.setRgbw(*tuple(rgb), w=white, brightness=brightness) + + # handle RGB mode + else: + self._bulb.setRgb(*tuple(rgb), brightness=brightness) + + if not self.is_on: + self._bulb.turnOn() def turn_off(self, **kwargs): """Turn the specified or all lights off.""" From 4208bb457d96e92dc952e69465ac6d2fc7e4fd1b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 27 Jun 2018 12:11:26 +0200 Subject: [PATCH 084/128] Upgrade youtube_dl to 2018.06.25 (#15168) --- 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 497b6f995bd..85895fdd751 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.06.14'] +REQUIREMENTS = ['youtube_dl==2018.06.25'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 6b9da71bd1f..84fa16a41cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1432,7 +1432,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.06.14 +youtube_dl==2018.06.25 # homeassistant.components.light.zengge zengge==0.2 From ba50a5c329e037062adfdd8071eb96d9c6a0d921 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 27 Jun 2018 12:11:41 +0200 Subject: [PATCH 085/128] Upgrade keyring to 13.0.0 (#15167) --- 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 e02305b5fbb..51d70d1f3b2 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.1', 'keyrings.alt==3.1'] +REQUIREMENTS = ['keyring==13.0.0', 'keyrings.alt==3.1'] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 84fa16a41cb..13b4364fe80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -476,7 +476,7 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.6 # homeassistant.scripts.keyring -keyring==12.2.1 +keyring==13.0.0 # homeassistant.scripts.keyring keyrings.alt==3.1 From 41017f10a3cc08f10372643a61e5b4697531dd7f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 27 Jun 2018 12:12:02 +0200 Subject: [PATCH 086/128] Upgrade sendgrid to 5.4.1 (#15166) --- homeassistant/components/notify/sendgrid.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index b73f3a17ee7..92b709af8ad 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT, CONTENT_TYPE_TEXT_PLAIN) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['sendgrid==5.4.0'] +REQUIREMENTS = ['sendgrid==5.4.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 13b4364fe80..24b1de60e1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1218,7 +1218,7 @@ schiene==0.22 scsgate==0.1.0 # homeassistant.components.notify.sendgrid -sendgrid==5.4.0 +sendgrid==5.4.1 # homeassistant.components.light.sensehat # homeassistant.components.sensor.sensehat From d6dee62c927d1e6c52c48fee35fff00731a39f12 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 27 Jun 2018 06:19:56 -0400 Subject: [PATCH 087/128] Add Mini remote support to insteon_plm (#15152) * Add mini-remote * Bump insteonplm version to 0.11.3 to support mini-remotes --- homeassistant/components/insteon_plm/__init__.py | 6 ++++-- requirements_all.txt | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py index 8197b45c28d..82fc6b02266 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.11.2'] +REQUIREMENTS = ['insteonplm==0.11.3'] _LOGGER = logging.getLogger(__name__) @@ -300,7 +300,8 @@ class IPDB(object): OpenClosedRelay) from insteonplm.states.dimmable import (DimmableSwitch, - DimmableSwitch_Fan) + DimmableSwitch_Fan, + DimmableRemote) from insteonplm.states.sensor import (VariableSensor, OnOffSensor, @@ -328,6 +329,7 @@ class IPDB(object): State(DimmableSwitch_Fan, 'fan'), State(DimmableSwitch, 'light'), + State(DimmableRemote, 'binary_sensor'), State(X10DimmableSwitch, 'light'), State(X10OnOffSwitch, 'switch'), diff --git a/requirements_all.txt b/requirements_all.txt index 24b1de60e1a..3d2d8ad2590 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -460,7 +460,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.11.2 +insteonplm==0.11.3 # homeassistant.components.sensor.iperf3 iperf3==0.1.10 From c0b6a857f7e6345817f82799a027268a85a075d6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Jun 2018 14:20:24 -0400 Subject: [PATCH 088/128] Version bump to 20180627.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 54a77af5cfb..ffdd3160b2e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180625.0'] +REQUIREMENTS = ['home-assistant-frontend==20180627.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 3d2d8ad2590..40b77e98613 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180625.0 +home-assistant-frontend==20180627.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9226b30498..e2cfced7d61 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==20180625.0 +home-assistant-frontend==20180627.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 742144f401075ce054c0b8b005e9d965b8af2a71 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Jun 2018 15:21:32 -0400 Subject: [PATCH 089/128] Warn when using custom components (#15172) * Warn when using custom components * Update text --- homeassistant/loader.py | 10 +++++++++- tests/test_loader.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ce93c8705b5..e3e41e09db2 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -81,7 +81,7 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: potential_paths = ['custom_components.{}'.format(comp_or_platform), 'homeassistant.components.{}'.format(comp_or_platform)] - for path in potential_paths: + for index, path in enumerate(potential_paths): try: module = importlib.import_module(path) @@ -100,6 +100,14 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: cache[comp_or_platform] = module + if index == 0: + _LOGGER.warning( + 'You are using a custom component for %s which has not ' + 'been tested by Home Assistant. This component might ' + 'cause stability problems, be sure to disable it if you ' + 'do experience issues with Home Assistant.', + comp_or_platform) + return module except ImportError as err: diff --git a/tests/test_loader.py b/tests/test_loader.py index c97e94a7ce1..d87201fb61b 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -124,3 +124,13 @@ async def test_custom_component_name(hass): # Test custom components is mounted from custom_components.test_package import TEST assert TEST == 5 + + +async def test_log_warning_custom_component(hass, caplog): + """Test that we log a warning when loading a custom component.""" + loader.get_component(hass, 'test_standalone') + assert \ + 'You are using a custom component for test_standalone' in caplog.text + + loader.get_component(hass, 'light.test') + assert 'You are using a custom component for light.test' in caplog.text From 9066ac44fe89607e1d02ec333605519b016b5eac Mon Sep 17 00:00:00 2001 From: MizterB <5458030+MizterB@users.noreply.github.com> Date: Wed, 27 Jun 2018 15:22:29 -0400 Subject: [PATCH 090/128] Philips Hue Scene Activation: Simplified scene lookup logic, improved error handling (#15175) * Simplified scene lookup logic, improved error handling * Lint --- homeassistant/components/hue/bridge.py | 28 +++++++++----------------- 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index d7a8dc7f730..8710b2561b0 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -124,24 +124,16 @@ class HueBridge(object): (group for group in self.api.groups.values() if group.name == group_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 + # Additional scene logic to handle duplicate scene names across groups + scene = next( + (scene for scene in self.api.scenes.values() + if scene.name == scene_name + and group is not None + and sorted(scene.lights) == sorted(group.lights)), + None) # If we can't find it, fetch latest info. - if not updated and (group is None or scene_id is None): + if not updated and (group is None or scene is None): await self.api.groups.update() await self.api.scenes.update() await self.hue_activate_scene(call, updated=True) @@ -151,11 +143,11 @@ class HueBridge(object): LOGGER.warning('Unable to find group %s', group_name) return - if scene_id is None: + if scene is None: LOGGER.warning('Unable to find scene %s', scene_name) return - await group.set_action(scene=scene_id) + await group.set_action(scene=scene.id) async def get_bridge(hass, host, username=None): From 4fbe3bb07062b81ac4562d4080550d92cbd47828 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Wed, 27 Jun 2018 13:55:27 -0700 Subject: [PATCH 091/128] Finalize BotVac D7 Support And Further Reduce Cloud Calls (#15161) * Finalize BotVac D7 Support And Further Reduce Cloud Calls * Lint * Lint Again * Implement requested changes * Hound * Lint --- homeassistant/components/camera/neato.py | 4 +-- homeassistant/components/neato.py | 31 +++++++++++++++++++++--- homeassistant/components/switch/neato.py | 4 +-- homeassistant/components/vacuum/neato.py | 8 ++++-- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/camera/neato.py b/homeassistant/components/camera/neato.py index 689129e1067..3a8a137c1fe 100644 --- a/homeassistant/components/camera/neato.py +++ b/homeassistant/components/camera/neato.py @@ -10,12 +10,13 @@ from datetime import timedelta from homeassistant.components.camera import Camera from homeassistant.components.neato import ( NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN) -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['neato'] +SCAN_INTERVAL = timedelta(minutes=10) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Neato Camera.""" @@ -45,7 +46,6 @@ class NeatoCleaningMap(Camera): self.update() return self._image - @Throttle(timedelta(seconds=60)) def update(self): """Check the contents of the map list.""" self.neato.update_robots() diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py index 6d14a6f3c4d..fc407de0a6b 100644 --- a/homeassistant/components/neato.py +++ b/homeassistant/components/neato.py @@ -54,7 +54,12 @@ ACTION = { 7: 'Updating...', 8: 'Copying logs...', 9: 'Calculating position...', - 10: 'IEC test' + 10: 'IEC test', + 11: 'Map cleaning', + 12: 'Exploring map (creating a persistent map)', + 13: 'Acquiring Persistent Map IDs', + 14: 'Creating & Uploading Map', + 15: 'Suspended Exploration' } ERRORS = { @@ -70,12 +75,30 @@ ERRORS = { 'ui_error_navigation_pathproblems_returninghome': 'Cannot return to base', 'ui_error_navigation_falling': 'Clear my path', 'ui_error_picked_up': 'Picked up', - 'ui_error_stuck': 'Stuck!' + 'ui_error_stuck': 'Stuck!', + 'dustbin_full': 'Dust bin full', + 'dustbin_missing': 'Dust bin missing', + 'maint_brush_stuck': 'Brush stuck', + 'maint_brush_overload': 'Brush overloaded', + 'maint_bumper_stuck': 'Bumper stuck', + 'maint_vacuum_stuck': 'Vacuum is stuck', + 'maint_left_drop_stuck': 'Vacuum is stuck', + 'maint_left_wheel_stuck': 'Vacuum is stuck', + 'maint_right_drop_stuck': 'Vacuum is stuck', + 'maint_right_wheel_stuck': 'Vacuum is stuck', + 'not_on_charge_base': 'Not on the charge base', + 'nav_robot_falling': 'Clear my path', + 'nav_no_path': 'Clear my path', + 'nav_path_problem': 'Clear my path' } ALERTS = { 'ui_alert_dust_bin_full': 'Please empty dust bin', - 'ui_alert_recovering_location': 'Returning to start' + 'ui_alert_recovering_location': 'Returning to start', + 'dustbin_full': 'Please empty dust bin', + 'maint_brush_change': 'Change the brush', + 'maint_filter_change': 'Change the filter', + 'clean_completed_to_start': 'Cleaning completed' } @@ -121,7 +144,7 @@ class NeatoHub(object): _LOGGER.error("Unable to connect to Neato API") return False - @Throttle(timedelta(seconds=60)) + @Throttle(timedelta(seconds=300)) def update_robots(self): """Update the robot states.""" _LOGGER.debug("Running HUB.update_robots %s", diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index 1d149383f6f..dca5d63b43d 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -10,12 +10,13 @@ import requests from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['neato'] +SCAN_INTERVAL = timedelta(minutes=10) + SWITCH_TYPE_SCHEDULE = 'schedule' SWITCH_TYPES = { @@ -52,7 +53,6 @@ class NeatoConnectedSwitch(ToggleEntity): self._schedule_state = None self._clean_state = None - @Throttle(timedelta(seconds=60)) def update(self): """Update the states of Neato switches.""" _LOGGER.debug("Running switch update") diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 128bece8494..1b32fff9e5b 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -15,12 +15,13 @@ from homeassistant.components.vacuum import ( SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON) from homeassistant.components.neato import ( NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['neato'] +SCAN_INTERVAL = timedelta(minutes=5) + SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ SUPPORT_STATUS | SUPPORT_MAP @@ -63,7 +64,6 @@ class NeatoConnectedVacuum(VacuumDevice): self.clean_suspension_charge_count = None self.clean_suspension_time = None - @Throttle(timedelta(seconds=60)) def update(self): """Update the states of Neato Vacuums.""" _LOGGER.debug("Running Neato Vacuums update") @@ -101,6 +101,10 @@ class NeatoConnectedVacuum(VacuumDevice): self.robot.state['action'] == 3 and self.robot.state['state'] == 2): self._clean_state = STATE_ON + elif (self.robot.state['action'] == 11 or + self.robot.state['action'] == 12 and + self.robot.state['state'] == 2): + self._clean_state = STATE_ON else: self._clean_state = STATE_OFF From dbb786c548bb21a0c2b5adfc1519f28155085466 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 28 Jun 2018 12:23:32 +0200 Subject: [PATCH 092/128] DarkSky weather / Fix states (#15174) * DarkSky weather / Fix states * fix lint * fix tests --- homeassistant/components/weather/darksky.py | 28 ++++++++++++++++++--- tests/components/weather/test_darksky.py | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index f0712542ea5..7afa97fd4f6 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -12,7 +12,8 @@ from requests.exceptions import ( import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_CONDITION, + PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -25,6 +26,22 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by Dark Sky" +MAP_CONDITION = { + 'clear-day': 'sunny', + 'clear-night': 'clear-night', + 'rain': 'rainy', + 'snow': 'snowy', + 'sleet': 'snowy-rainy', + 'wind': 'windy', + 'fog': 'fog', + 'cloudy': 'cloudy', + 'partly-cloudy-day': 'partlycloudy', + 'partly-cloudy-night': 'partlycloudy', + 'hail': 'hail', + 'thunderstorm': 'lightning', + 'tornado': None, +} + CONF_UNITS = 'units' DEFAULT_NAME = 'Dark Sky' @@ -108,7 +125,7 @@ class DarkSkyWeather(WeatherEntity): @property def condition(self): """Return the weather condition.""" - return self._ds_currently.get('summary') + return MAP_CONDITION.get(self._ds_currently.get('icon')) @property def forecast(self): @@ -116,8 +133,11 @@ class DarkSkyWeather(WeatherEntity): return [{ ATTR_FORECAST_TIME: datetime.fromtimestamp(entry.d.get('time')).isoformat(), - ATTR_FORECAST_TEMP: entry.d.get('temperature')} - for entry in self._ds_hourly.data] + ATTR_FORECAST_TEMP: + entry.d.get('temperature'), + ATTR_FORECAST_CONDITION: + MAP_CONDITION.get(entry.d.get('icon')) + } for entry in self._ds_hourly.data] def update(self): """Get the latest data from Dark Sky.""" diff --git a/tests/components/weather/test_darksky.py b/tests/components/weather/test_darksky.py index 7faa033e0a8..41687451cd6 100644 --- a/tests/components/weather/test_darksky.py +++ b/tests/components/weather/test_darksky.py @@ -48,4 +48,4 @@ class TestDarkSky(unittest.TestCase): self.assertEqual(mock_get_forecast.call_count, 1) state = self.hass.states.get('weather.test') - self.assertEqual(state.state, 'Clear') + self.assertEqual(state.state, 'sunny') From 19f2bbf52f2ff947699ebd8e513ba2da6ebc6241 Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Thu, 28 Jun 2018 09:16:11 -0400 Subject: [PATCH 093/128] Only use the X-Forwarded-For header if connection is from a trusted network (#15182) See https://github.com/home-assistant/home-assistant/issues/14345#issuecomment-400854569 --- homeassistant/components/http/__init__.py | 2 +- homeassistant/components/http/real_ip.py | 14 +++++++++----- tests/components/http/test_auth.py | 2 +- tests/components/http/test_real_ip.py | 23 ++++++++++++++++++++--- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index d8c877e83a2..f769d2bc4ff 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -180,7 +180,7 @@ class HomeAssistantHTTP(object): middlewares=[staticresource_middleware]) # This order matters - setup_real_ip(app, use_x_forwarded_for) + setup_real_ip(app, use_x_forwarded_for, trusted_networks) if is_ban_enabled: setup_bans(hass, app, login_threshold) diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py index c394016a683..401a09dc306 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -11,18 +11,22 @@ from .const import KEY_REAL_IP @callback -def setup_real_ip(app, use_x_forwarded_for): +def setup_real_ip(app, use_x_forwarded_for, trusted_networks): """Create IP Ban middleware for the app.""" @middleware async def real_ip_middleware(request, handler): """Real IP middleware.""" + connected_ip = ip_address( + request.transport.get_extra_info('peername')[0]) + request[KEY_REAL_IP] = connected_ip + + # Only use the XFF header if enabled, present, and from a trusted proxy if (use_x_forwarded_for and - X_FORWARDED_FOR in request.headers): + X_FORWARDED_FOR in request.headers and + any(connected_ip in trusted_network + for trusted_network in trusted_networks)): request[KEY_REAL_IP] = ip_address( request.headers.get(X_FORWARDED_FOR).split(',')[0]) - else: - request[KEY_REAL_IP] = \ - ip_address(request.transport.get_extra_info('peername')[0]) return await handler(request) diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index a44d17d513d..dd8b2cd35c4 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -41,7 +41,7 @@ def app(): """Fixture to setup a web.Application.""" app = web.Application() app.router.add_get('/', mock_handler) - setup_real_ip(app, False) + setup_real_ip(app, False, []) return app diff --git a/tests/components/http/test_real_ip.py b/tests/components/http/test_real_ip.py index 61846eb94c2..b6af8159207 100644 --- a/tests/components/http/test_real_ip.py +++ b/tests/components/http/test_real_ip.py @@ -1,6 +1,7 @@ """Test real IP middleware.""" from aiohttp import web from aiohttp.hdrs import X_FORWARDED_FOR +from ipaddress import ip_network from homeassistant.components.http.real_ip import setup_real_ip from homeassistant.components.http.const import KEY_REAL_IP @@ -15,7 +16,7 @@ async def test_ignore_x_forwarded_for(aiohttp_client): """Test that we get the IP from the transport.""" app = web.Application() app.router.add_get('/', mock_handler) - setup_real_ip(app, False) + setup_real_ip(app, False, []) mock_api_client = await aiohttp_client(app) @@ -27,11 +28,27 @@ async def test_ignore_x_forwarded_for(aiohttp_client): assert text != '255.255.255.255' -async def test_use_x_forwarded_for(aiohttp_client): +async def test_use_x_forwarded_for_without_trusted_proxy(aiohttp_client): """Test that we get the IP from the transport.""" app = web.Application() app.router.add_get('/', mock_handler) - setup_real_ip(app, True) + setup_real_ip(app, True, []) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get('/', headers={ + X_FORWARDED_FOR: '255.255.255.255' + }) + assert resp.status == 200 + text = await resp.text() + assert text != '255.255.255.255' + + +async def test_use_x_forwarded_for_with_trusted_proxy(aiohttp_client): + """Test that we get the IP from the transport.""" + app = web.Application() + app.router.add_get('/', mock_handler) + setup_real_ip(app, True, [ip_network('127.0.0.1')]) mock_api_client = await aiohttp_client(app) From a277470363c0758bb305410aad49c257ff8bac40 Mon Sep 17 00:00:00 2001 From: Alex Barcelo Date: Thu, 28 Jun 2018 16:49:33 +0200 Subject: [PATCH 094/128] Adding 'namespace' for prometheus metrics (#13738) * Updating prometheus client version * Using `entity_filter` as filter mechanism * New optional `namespace` configuration --- homeassistant/components/prometheus.py | 49 +++++++++++++------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/test_prometheus.py | 2 +- 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index 96ed098567d..6f233dafe08 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -11,16 +11,15 @@ import voluptuous as vol from aiohttp import web from homeassistant.components.http import HomeAssistantView -from homeassistant.components import recorder from homeassistant.const import ( - CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, CONTENT_TYPE_TEXT_PLAIN, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT) from homeassistant import core as hacore -from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import entityfilter, state as state_helper from homeassistant.util.temperature import fahrenheit_to_celsius -REQUIREMENTS = ['prometheus_client==0.1.0'] +REQUIREMENTS = ['prometheus_client==0.2.0'] _LOGGER = logging.getLogger(__name__) @@ -29,8 +28,14 @@ API_ENDPOINT = '/api/prometheus' DOMAIN = 'prometheus' DEPENDENCIES = ['http'] +CONF_FILTER = 'filter' +CONF_PROM_NAMESPACE = 'namespace' + CONFIG_SCHEMA = vol.Schema({ - DOMAIN: recorder.FILTER_SCHEMA, + DOMAIN: vol.All({ + vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, + vol.Optional(CONF_PROM_NAMESPACE): cv.string, + }) }, extra=vol.ALLOW_EXTRA) @@ -40,25 +45,26 @@ def setup(hass, config): hass.http.register_view(PrometheusView(prometheus_client)) - conf = config.get(DOMAIN, {}) - exclude = conf.get(CONF_EXCLUDE, {}) - include = conf.get(CONF_INCLUDE, {}) - metrics = Metrics(prometheus_client, exclude, include) + conf = config[DOMAIN] + entity_filter = conf[CONF_FILTER] + namespace = conf.get(CONF_PROM_NAMESPACE) + metrics = PrometheusMetrics(prometheus_client, entity_filter, namespace) hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_event) return True -class Metrics(object): +class PrometheusMetrics(object): """Model all of the metrics which should be exposed to Prometheus.""" - def __init__(self, prometheus_client, exclude, include): + def __init__(self, prometheus_client, entity_filter, namespace): """Initialize Prometheus Metrics.""" self.prometheus_client = prometheus_client - self.exclude = exclude.get(CONF_ENTITIES, []) + \ - exclude.get(CONF_DOMAINS, []) - self.include_domains = include.get(CONF_DOMAINS, []) - self.include_entities = include.get(CONF_ENTITIES, []) + self._filter = entity_filter + if namespace: + self.metrics_prefix = "{}_".format(namespace) + else: + self.metrics_prefix = "" self._metrics = {} def handle_event(self, event): @@ -71,14 +77,7 @@ class Metrics(object): _LOGGER.debug("Handling state update for %s", entity_id) domain, _ = hacore.split_entity_id(entity_id) - if entity_id in self.exclude: - return - if domain in self.exclude and entity_id not in self.include_entities: - return - if self.include_domains and domain not in self.include_domains: - return - if not self.exclude and (self.include_entities and - entity_id not in self.include_entities): + if not self._filter(state.entity_id): return handler = '_handle_{}'.format(domain) @@ -100,7 +99,9 @@ class Metrics(object): try: return self._metrics[metric] except KeyError: - self._metrics[metric] = factory(metric, documentation, labels) + full_metric_name = "{}{}".format(self.metrics_prefix, metric) + self._metrics[metric] = factory( + full_metric_name, documentation, labels) return self._metrics[metric] @staticmethod diff --git a/requirements_all.txt b/requirements_all.txt index 40b77e98613..1a3b6cacf9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -674,7 +674,7 @@ postnl_api==1.0.2 proliphix==0.4.1 # homeassistant.components.prometheus -prometheus_client==0.1.0 +prometheus_client==0.2.0 # homeassistant.components.sensor.systemmonitor psutil==5.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2cfced7d61..1ae0a5db6c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -120,7 +120,7 @@ pilight==0.1.1 pmsensor==0.4 # homeassistant.components.prometheus -prometheus_client==0.1.0 +prometheus_client==0.2.0 # homeassistant.components.notify.pushbullet # homeassistant.components.sensor.pushbullet diff --git a/tests/components/test_prometheus.py b/tests/components/test_prometheus.py index e336a28eb03..49744421c72 100644 --- a/tests/components/test_prometheus.py +++ b/tests/components/test_prometheus.py @@ -12,7 +12,7 @@ def prometheus_client(loop, hass, aiohttp_client): assert loop.run_until_complete(async_setup_component( hass, prometheus.DOMAIN, - {}, + {prometheus.DOMAIN: {}}, )) return loop.run_until_complete(aiohttp_client(hass.http.app)) From 2205090795c7a67b1d764a59ec5cfcb20956c0c1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Jun 2018 22:14:26 -0400 Subject: [PATCH 095/128] Storage auth (#15192) * Support parallel loading * Add storage mock * Store auth * Fix tests --- homeassistant/auth.py | 143 +++++++++++++++--- homeassistant/helpers/storage.py | 21 ++- tests/auth_providers/test_insecure_example.py | 8 +- tests/common.py | 51 ++++++- tests/conftest.py | 12 +- tests/helpers/test_storage.py | 105 ++++++++----- tests/test_auth.py | 48 +++++- tests/test_config_entries.py | 28 ++-- 8 files changed, 324 insertions(+), 92 deletions(-) diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 5e434b74ca8..0c8346607ca 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -21,6 +21,8 @@ from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) +STORAGE_VERSION = 1 +STORAGE_KEY = 'auth' AUTH_PROVIDERS = Registry() @@ -121,23 +123,12 @@ class User: is_owner = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False) name = attr.ib(type=str, default=None) - # For persisting and see if saved? - # store = attr.ib(type=AuthStore, default=None) # List of credentials of a user. - credentials = attr.ib(type=list, default=attr.Factory(list)) + credentials = attr.ib(type=list, default=attr.Factory(list), cmp=False) # Tokens associated with a user. - refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict)) - - def as_dict(self): - """Convert user object to a dictionary.""" - return { - 'id': self.id, - 'is_owner': self.is_owner, - 'is_active': self.is_active, - 'name': self.name, - } + refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict), cmp=False) @attr.s(slots=True) @@ -152,7 +143,7 @@ class RefreshToken: default=ACCESS_TOKEN_EXPIRATION) token = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64))) - access_tokens = attr.ib(type=list, default=attr.Factory(list)) + access_tokens = attr.ib(type=list, default=attr.Factory(list), cmp=False) @attr.s(slots=True) @@ -376,7 +367,7 @@ class AuthStore: self.hass = hass self.users = None self.clients = None - self._load_lock = asyncio.Lock(loop=hass.loop) + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) async def credentials_for_provider(self, provider_type, provider_id): """Return credentials for specific auth provider type and id.""" @@ -494,10 +485,128 @@ class AuthStore: async def async_load(self): """Load the users.""" - async with self._load_lock: + data = await self._store.async_load() + + # Make sure that we're not overriding data if 2 loads happened at the + # same time + if self.users is not None: + return + + if data is None: self.users = {} self.clients = {} + return + + users = { + user_dict['id']: User(**user_dict) for user_dict in data['users'] + } + + for cred_dict in data['credentials']: + users[cred_dict['user_id']].credentials.append(Credentials( + id=cred_dict['id'], + is_new=False, + auth_provider_type=cred_dict['auth_provider_type'], + auth_provider_id=cred_dict['auth_provider_id'], + data=cred_dict['data'], + )) + + refresh_tokens = {} + + for rt_dict in data['refresh_tokens']: + token = RefreshToken( + id=rt_dict['id'], + user=users[rt_dict['user_id']], + client_id=rt_dict['client_id'], + created_at=dt_util.parse_datetime(rt_dict['created_at']), + access_token_expiration=timedelta( + rt_dict['access_token_expiration']), + token=rt_dict['token'], + ) + refresh_tokens[token.id] = token + users[rt_dict['user_id']].refresh_tokens[token.token] = token + + for ac_dict in data['access_tokens']: + refresh_token = refresh_tokens[ac_dict['refresh_token_id']] + token = AccessToken( + refresh_token=refresh_token, + created_at=dt_util.parse_datetime(ac_dict['created_at']), + token=ac_dict['token'], + ) + refresh_token.access_tokens.append(token) + + clients = { + cl_dict['id']: Client(**cl_dict) for cl_dict in data['clients'] + } + + self.users = users + self.clients = clients async def async_save(self): """Save users.""" - pass + users = [ + { + 'id': user.id, + 'is_owner': user.is_owner, + 'is_active': user.is_active, + 'name': user.name, + } + for user in self.users.values() + ] + + credentials = [ + { + 'id': credential.id, + 'user_id': user.id, + 'auth_provider_type': credential.auth_provider_type, + 'auth_provider_id': credential.auth_provider_id, + 'data': credential.data, + } + for user in self.users.values() + for credential in user.credentials + ] + + refresh_tokens = [ + { + 'id': refresh_token.id, + 'user_id': user.id, + 'client_id': refresh_token.client_id, + 'created_at': refresh_token.created_at.isoformat(), + 'access_token_expiration': + refresh_token.access_token_expiration.total_seconds(), + 'token': refresh_token.token, + } + for user in self.users.values() + for refresh_token in user.refresh_tokens.values() + ] + + access_tokens = [ + { + 'id': user.id, + 'refresh_token_id': refresh_token.id, + 'created_at': access_token.created_at.isoformat(), + 'token': access_token.token, + } + for user in self.users.values() + for refresh_token in user.refresh_tokens.values() + for access_token in refresh_token.access_tokens + ] + + clients = [ + { + 'id': client.id, + 'name': client.name, + 'secret': client.secret, + 'redirect_uris': client.redirect_uris, + } + for client in self.clients.values() + ] + + data = { + 'users': users, + 'clients': clients, + 'credentials': credentials, + 'access_tokens': access_tokens, + 'refresh_tokens': refresh_tokens, + } + + await self._store.async_save(data, delay=1) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 18c3ddf7fcd..962074ec3af 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -53,6 +53,7 @@ class Store: self._unsub_delay_listener = None self._unsub_stop_listener = None self._write_lock = asyncio.Lock() + self._load_task = None @property def path(self): @@ -64,7 +65,17 @@ class Store: If the expected version does not match the given version, the migrate function will be invoked with await migrate_func(version, config). + + Will ensure that when a call comes in while another one is in progress, + the second call will wait and return the result of the first call. """ + if self._load_task is None: + self._load_task = self.hass.async_add_job(self._async_load()) + + return await self._load_task + + async def _async_load(self): + """Helper to load the data.""" if self._data is not None: data = self._data else: @@ -75,9 +86,15 @@ class Store: return None if data['version'] == self.version: - return data['data'] + stored = data['data'] + else: + _LOGGER.info('Migrating %s storage from %s to %s', + self.key, data['version'], self.version) + stored = await self._async_migrate_func( + data['version'], data['data']) - return await self._async_migrate_func(data['version'], data['data']) + self._load_task = None + return stored async def async_save(self, data: Dict, *, delay: Optional[int] = None): """Save data with an optional delay.""" diff --git a/tests/auth_providers/test_insecure_example.py b/tests/auth_providers/test_insecure_example.py index 0b481f93099..3377a60c45b 100644 --- a/tests/auth_providers/test_insecure_example.py +++ b/tests/auth_providers/test_insecure_example.py @@ -11,15 +11,15 @@ from tests.common import mock_coro @pytest.fixture -def store(): +def store(hass): """Mock store.""" - return auth.AuthStore(Mock()) + return auth.AuthStore(hass) @pytest.fixture -def provider(store): +def provider(hass, store): """Mock provider.""" - return insecure_example.ExampleAuthProvider(None, store, { + return insecure_example.ExampleAuthProvider(hass, store, { 'type': 'insecure_example', 'users': [ { diff --git a/tests/common.py b/tests/common.py index 56575bdb1e9..8eaee686b22 100644 --- a/tests/common.py +++ b/tests/common.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import functools as ft +import json import os import sys from unittest.mock import patch, MagicMock, Mock @@ -15,7 +16,7 @@ from homeassistant.setup import setup_component, async_setup_component from homeassistant.config import async_process_component_config from homeassistant.helpers import ( intent, entity, restore_state, entity_registry, - entity_platform) + entity_platform, storage) from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.dt as date_util import homeassistant.util.yaml as yaml @@ -705,3 +706,51 @@ class MockEntity(entity.Entity): if attr in self._values: return self._values[attr] return getattr(super(), attr) + + +@contextmanager +def mock_storage(data=None): + """Mock storage. + + Data is a dict {'key': {'version': version, 'data': data}} + + Written data will be converted to JSON to ensure JSON parsing works. + """ + if data is None: + data = {} + + orig_load = storage.Store._async_load + + async def mock_async_load(store): + """Mock version of load.""" + if store._data is None: + # No data to load + if store.key not in data: + return None + + store._data = data.get(store.key) + + # Route through original load so that we trigger migration + loaded = await orig_load(store) + _LOGGER.info('Loading data for %s: %s', store.key, loaded) + return loaded + + def mock_write_data(store, path, data_to_write): + """Mock version of write data.""" + # To ensure that the data can be serialized + _LOGGER.info('Writing data to %s: %s', store.key, data_to_write) + data[store.key] = json.loads(json.dumps(data_to_write)) + + with patch('homeassistant.helpers.storage.Store._async_load', + side_effect=mock_async_load, autospec=True), \ + patch('homeassistant.helpers.storage.Store._write_data', + side_effect=mock_write_data, autospec=True): + yield data + + +async def flush_store(store): + """Make sure all delayed writes of a store are written.""" + if store._data is None: + return + + await store._async_handle_write_data() diff --git a/tests/conftest.py b/tests/conftest.py index 4d619c5ef61..0a350b62fc1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,8 @@ from homeassistant import util from homeassistant.util import location from tests.common import ( - async_test_home_assistant, INSTANCES, async_mock_mqtt_component, mock_coro) + async_test_home_assistant, INSTANCES, async_mock_mqtt_component, mock_coro, + mock_storage as mock_storage) from tests.test_util.aiohttp import mock_aiohttp_client from tests.mock.zwave import MockNetwork, MockOption @@ -59,7 +60,14 @@ def verify_cleanup(): @pytest.fixture -def hass(loop): +def hass_storage(): + """Fixture to mock storage.""" + with mock_storage() as stored_data: + yield stored_data + + +@pytest.fixture +def hass(loop, hass_storage): """Fixture to provide a test instance of HASS.""" hass = loop.run_until_complete(async_test_home_assistant(loop)) diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 04de920b036..f414eaec97c 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -1,4 +1,5 @@ """Tests for the storage helper.""" +import asyncio from datetime import timedelta from unittest.mock import patch @@ -16,32 +17,13 @@ MOCK_KEY = 'storage-test' MOCK_DATA = {'hello': 'world'} -@pytest.fixture -def mock_save(): - """Fixture to mock JSON save.""" - written = [] - with patch('homeassistant.util.json.save_json', - side_effect=lambda *args: written.append(args)): - yield written - - -@pytest.fixture -def mock_load(mock_save): - """Fixture to mock JSON read.""" - with patch('homeassistant.util.json.load_json', - side_effect=lambda *args: mock_save[-1][1]): - yield - - @pytest.fixture def store(hass): """Fixture of a store that prevents writing on HASS stop.""" - store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) - store._async_ensure_stop_listener = lambda: None - yield store + yield storage.Store(hass, MOCK_VERSION, MOCK_KEY) -async def test_loading(hass, store, mock_save, mock_load): +async def test_loading(hass, store): """Test we can save and load data.""" await store.async_save(MOCK_DATA) data = await store.async_load() @@ -55,55 +37,96 @@ async def test_loading_non_existing(hass, store): assert data is None -async def test_saving_with_delay(hass, store, mock_save): +async def test_loading_parallel(hass, store, hass_storage, caplog): + """Test we can save and load data.""" + hass_storage[store.key] = { + 'version': MOCK_VERSION, + 'data': MOCK_DATA, + } + + results = await asyncio.gather( + store.async_load(), + store.async_load() + ) + + assert results[0] is MOCK_DATA + assert results[1] is MOCK_DATA + assert caplog.text.count('Loading data for {}'.format(store.key)) + + +async def test_saving_with_delay(hass, store, hass_storage): """Test saving data after a delay.""" await store.async_save(MOCK_DATA, delay=1) - assert len(mock_save) == 0 + assert store.key not in hass_storage async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(mock_save) == 1 + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': MOCK_DATA, + } -async def test_saving_on_stop(hass, mock_save): +async def test_saving_on_stop(hass, hass_storage): """Test delayed saves trigger when we quit Home Assistant.""" store = storage.Store(hass, MOCK_VERSION, MOCK_KEY) await store.async_save(MOCK_DATA, delay=1) - assert len(mock_save) == 0 + assert store.key not in hass_storage hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - assert len(mock_save) == 1 + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': MOCK_DATA, + } -async def test_loading_while_delay(hass, store, mock_save, mock_load): +async def test_loading_while_delay(hass, store, hass_storage): """Test we load new data even if not written yet.""" await store.async_save({'delay': 'no'}) - assert len(mock_save) == 1 + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': {'delay': 'no'}, + } await store.async_save({'delay': 'yes'}, delay=1) - assert len(mock_save) == 1 + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': {'delay': 'no'}, + } data = await store.async_load() assert data == {'delay': 'yes'} -async def test_writing_while_writing_delay(hass, store, mock_save, mock_load): +async def test_writing_while_writing_delay(hass, store, hass_storage): """Test a write while a write with delay is active.""" await store.async_save({'delay': 'yes'}, delay=1) - assert len(mock_save) == 0 + assert store.key not in hass_storage await store.async_save({'delay': 'no'}) - assert len(mock_save) == 1 + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': {'delay': 'no'}, + } async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) await hass.async_block_till_done() - assert len(mock_save) == 1 + assert hass_storage[store.key] == { + 'version': MOCK_VERSION, + 'key': MOCK_KEY, + 'data': {'delay': 'no'}, + } data = await store.async_load() assert data == {'delay': 'no'} -async def test_migrator_no_existing_config(hass, store, mock_save): +async def test_migrator_no_existing_config(hass, store, hass_storage): """Test migrator with no existing config.""" with patch('os.path.isfile', return_value=False), \ patch.object(store, 'async_load', @@ -112,10 +135,10 @@ async def test_migrator_no_existing_config(hass, store, mock_save): hass, 'old-path', store) assert data == {'cur': 'config'} - assert len(mock_save) == 0 + assert store.key not in hass_storage -async def test_migrator_existing_config(hass, store, mock_save): +async def test_migrator_existing_config(hass, store, hass_storage): """Test migrating existing config.""" with patch('os.path.isfile', return_value=True), \ patch('os.remove') as mock_remove, \ @@ -126,15 +149,14 @@ async def test_migrator_existing_config(hass, store, mock_save): assert len(mock_remove.mock_calls) == 1 assert data == {'old': 'config'} - assert len(mock_save) == 1 - assert mock_save[0][1] == { + assert hass_storage[store.key] == { 'key': MOCK_KEY, 'version': MOCK_VERSION, 'data': data, } -async def test_migrator_transforming_config(hass, store, mock_save): +async def test_migrator_transforming_config(hass, store, hass_storage): """Test migrating config to new format.""" async def old_conf_migrate_func(old_config): """Migrate old config to new format.""" @@ -150,8 +172,7 @@ async def test_migrator_transforming_config(hass, store, mock_save): assert len(mock_remove.mock_calls) == 1 assert data == {'new': 'config'} - assert len(mock_save) == 1 - assert mock_save[0][1] == { + assert hass_storage[store.key] == { 'key': MOCK_KEY, 'version': MOCK_VERSION, 'data': data, diff --git a/tests/test_auth.py b/tests/test_auth.py index 4bbf218fd23..116f92ca817 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -4,7 +4,7 @@ from unittest.mock import Mock import pytest from homeassistant import auth, data_entry_flow -from tests.common import MockUser, ensure_auth_manager_loaded +from tests.common import MockUser, ensure_auth_manager_loaded, flush_store @pytest.fixture @@ -53,9 +53,9 @@ async def test_auth_manager_from_config_validates_config_and_id(mock_hass): }] -async def test_create_new_user(mock_hass): +async def test_create_new_user(hass, hass_storage): """Test creating new user.""" - manager = await auth.auth_manager_from_config(mock_hass, [{ + manager = await auth.auth_manager_from_config(hass, [{ 'type': 'insecure_example', 'users': [{ 'username': 'test-user', @@ -124,9 +124,9 @@ async def test_login_as_existing_user(mock_hass): assert user.name == 'Paulus' -async def test_linking_user_to_two_auth_providers(mock_hass): +async def test_linking_user_to_two_auth_providers(hass, hass_storage): """Test linking user to two auth providers.""" - manager = await auth.auth_manager_from_config(mock_hass, [{ + manager = await auth.auth_manager_from_config(hass, [{ 'type': 'insecure_example', 'users': [{ 'username': 'test-user', @@ -157,3 +157,41 @@ async def test_linking_user_to_two_auth_providers(mock_hass): }) await manager.async_link_user(user, step['result']) assert len(user.credentials) == 2 + + +async def test_saving_loading(hass, hass_storage): + """Test storing and saving data. + + Creates one of each type that we store to test we restore correctly. + """ + manager = await auth.auth_manager_from_config(hass, [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + }] + }]) + + step = await manager.login_flow.async_init(('insecure_example', None)) + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'test-user', + 'password': 'test-pass', + }) + user = await manager.async_get_or_create_user(step['result']) + + client = await manager.async_create_client( + 'test', redirect_uris=['https://example.com']) + + refresh_token = await manager.async_create_refresh_token(user, client.id) + + manager.async_create_access_token(refresh_token) + + await flush_store(manager._store._store) + + store2 = auth.AuthStore(hass) + await store2.async_load() + assert len(store2.users) == 1 + assert store2.users[user.id] == user + + assert len(store2.clients) == 1 + assert store2.clients[client.id] == client diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b65e0dd62e7..d7a7ec4b82b 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,7 +1,7 @@ """Test the config manager.""" import asyncio from datetime import timedelta -from unittest.mock import MagicMock, patch, mock_open +from unittest.mock import MagicMock, patch import pytest @@ -152,8 +152,7 @@ def test_domains_gets_uniques(manager): assert manager.async_domains() == ['test', 'test2', 'test3'] -@asyncio.coroutine -def test_saving_and_loading(hass): +async def test_saving_and_loading(hass): """Test that we're saving and loading correctly.""" loader.set_component( hass, 'test', @@ -172,7 +171,7 @@ def test_saving_and_loading(hass): ) with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): - yield from hass.config_entries.flow.async_init('test') + await hass.config_entries.flow.async_init('test') class Test2Flow(data_entry_flow.FlowHandler): VERSION = 3 @@ -186,27 +185,18 @@ def test_saving_and_loading(hass): } ) - json_path = 'homeassistant.util.json.open' - with patch('homeassistant.config_entries.HANDLERS.get', return_value=Test2Flow): - yield from hass.config_entries.flow.async_init('test') + await hass.config_entries.flow.async_init('test') - with patch(json_path, mock_open(), create=True) as mock_write: - # To trigger the call_later - async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) - # To execute the save - yield from hass.async_block_till_done() - - # Mock open calls are: open file, context enter, write, context leave - written = mock_write.mock_calls[2][1][0] + # To trigger the call_later + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) + # To execute the save + await hass.async_block_till_done() # Now load written data in new config manager manager = config_entries.ConfigEntries(hass, {}) - - with patch('os.path.isfile', return_value=False), \ - patch(json_path, mock_open(read_data=written), create=True): - yield from manager.async_load() + await manager.async_load() # Ensure same order for orig, loaded in zip(hass.config_entries.async_entries(), From 39971ee9190b616fc3149c53912f9f8b2976c46a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jun 2018 00:02:33 -0400 Subject: [PATCH 096/128] Make sure we check access token expiration (#15207) * Make sure we check access token expiration * Use correct access token websocket --- homeassistant/auth.py | 27 +++++++--- homeassistant/components/frontend/__init__.py | 2 +- homeassistant/components/websocket_api.py | 5 +- tests/common.py | 1 + tests/test_auth.py | 50 ++++++++++++++++++- 5 files changed, 74 insertions(+), 11 deletions(-) diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 0c8346607ca..22abcdf213c 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -159,9 +159,10 @@ class AccessToken: default=attr.Factory(generate_secret)) @property - def expires(self): - """Return datetime when this token expires.""" - return self.created_at + self.refresh_token.access_token_expiration + def expired(self): + """Return if this token has expired.""" + expires = self.created_at + self.refresh_token.access_token_expiration + return dt_util.utcnow() > expires @attr.s(slots=True) @@ -272,7 +273,12 @@ class AuthManager: self.login_flow = data_entry_flow.FlowManager( hass, self._async_create_login_flow, self._async_finish_login_flow) - self.access_tokens = {} + self._access_tokens = {} + + @property + def active(self): + """Return if any auth providers are registered.""" + return bool(self._providers) @property def async_auth_providers(self): @@ -308,13 +314,22 @@ class AuthManager: def async_create_access_token(self, refresh_token): """Create a new access token.""" access_token = AccessToken(refresh_token) - self.access_tokens[access_token.token] = access_token + self._access_tokens[access_token.token] = access_token return access_token @callback def async_get_access_token(self, token): """Get an access token.""" - return self.access_tokens.get(token) + tkn = self._access_tokens.get(token) + + if tkn is None: + return None + + if tkn.expired: + self._access_tokens.pop(token) + return None + + return tkn async def async_create_client(self, name, *, redirect_uris=None, no_secret=False): diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index ffdd3160b2e..0e9d7612669 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -200,7 +200,7 @@ def add_manifest_json_key(key, val): async def async_setup(hass, config): """Set up the serving of the frontend.""" - if list(hass.auth.async_auth_providers): + if hass.auth.active: client = await hass.auth.async_create_client( 'Home Assistant Frontend', redirect_uris=['/'], diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index aacef4547b7..bf472348bab 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -324,8 +324,9 @@ class ActiveConnection: request, msg['api_password']) elif 'access_token' in msg: - authenticated = \ - msg['access_token'] in self.hass.auth.access_tokens + token = self.hass.auth.async_get_access_token( + msg['access_token']) + authenticated = token is not None if not authenticated: self.debug("Invalid password") diff --git a/tests/common.py b/tests/common.py index 8eaee686b22..1b8eabaa0db 100644 --- a/tests/common.py +++ b/tests/common.py @@ -320,6 +320,7 @@ class MockUser(auth.User): def add_to_auth_manager(self, auth_mgr): """Test helper to add entry to hass.""" + ensure_auth_manager_loaded(auth_mgr) auth_mgr._store.users[self.id] = self return self diff --git a/tests/test_auth.py b/tests/test_auth.py index 116f92ca817..4c0db71466e 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,14 +1,16 @@ """Tests for the Home Assistant auth module.""" -from unittest.mock import Mock +from datetime import timedelta +from unittest.mock import Mock, patch import pytest from homeassistant import auth, data_entry_flow +from homeassistant.util import dt as dt_util from tests.common import MockUser, ensure_auth_manager_loaded, flush_store @pytest.fixture -def mock_hass(): +def mock_hass(loop): """Hass mock with minimum amount of data set to make it work with auth.""" hass = Mock() hass.config.skip_pip = True @@ -195,3 +197,47 @@ async def test_saving_loading(hass, hass_storage): assert len(store2.clients) == 1 assert store2.clients[client.id] == client + + +def test_access_token_expired(): + """Test that the expired property on access tokens work.""" + refresh_token = auth.RefreshToken( + user=None, + client_id='bla' + ) + + access_token = auth.AccessToken( + refresh_token=refresh_token + ) + + assert access_token.expired is False + + with patch('homeassistant.auth.dt_util.utcnow', + return_value=dt_util.utcnow() + auth.ACCESS_TOKEN_EXPIRATION): + assert access_token.expired is True + + almost_exp = dt_util.utcnow() + auth.ACCESS_TOKEN_EXPIRATION - timedelta(1) + with patch('homeassistant.auth.dt_util.utcnow', return_value=almost_exp): + assert access_token.expired is False + + +async def test_cannot_retrieve_expired_access_token(hass): + """Test that we cannot retrieve expired access tokens.""" + manager = await auth.auth_manager_from_config(hass, []) + user = MockUser( + id='mock-user', + is_owner=False, + is_active=False, + name='Paulus', + ).add_to_auth_manager(manager) + refresh_token = await manager.async_create_refresh_token(user, 'bla') + access_token = manager.async_create_access_token(refresh_token) + + assert manager.async_get_access_token(access_token.token) is access_token + + with patch('homeassistant.auth.dt_util.utcnow', + return_value=dt_util.utcnow() + auth.ACCESS_TOKEN_EXPIRATION): + assert manager.async_get_access_token(access_token.token) is None + + # Even with unpatched time, it should have been removed from manager + assert manager.async_get_access_token(access_token.token) is None From 26590e244ced1b67792bac8fdd5fc81ac446dba1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jun 2018 00:02:45 -0400 Subject: [PATCH 097/128] Migrate home assistant auth provider to use storage helper (#15200) --- homeassistant/auth_providers/homeassistant.py | 39 ++++---- homeassistant/scripts/auth.py | 25 +++-- tests/auth_providers/test_homeassistant.py | 93 +++++++------------ tests/scripts/test_auth.py | 72 ++++++++------ 4 files changed, 113 insertions(+), 116 deletions(-) diff --git a/homeassistant/auth_providers/homeassistant.py b/homeassistant/auth_providers/homeassistant.py index c2db193ce1a..c4d2021f6ce 100644 --- a/homeassistant/auth_providers/homeassistant.py +++ b/homeassistant/auth_providers/homeassistant.py @@ -8,10 +8,10 @@ import voluptuous as vol from homeassistant import auth, data_entry_flow from homeassistant.exceptions import HomeAssistantError -from homeassistant.util import json -PATH_DATA = '.users.json' +STORAGE_VERSION = 1 +STORAGE_KEY = 'auth_provider.homeassistant' CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ }, extra=vol.PREVENT_EXTRA) @@ -31,14 +31,22 @@ class InvalidUser(HomeAssistantError): class Data: """Hold the user data.""" - def __init__(self, path, data): + def __init__(self, hass): """Initialize the user data store.""" - self.path = path + self.hass = hass + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._data = None + + async def async_load(self): + """Load stored data.""" + data = await self._store.async_load() + if data is None: data = { 'salt': auth.generate_secret(), 'users': [] } + self._data = data @property @@ -99,14 +107,9 @@ class Data: else: raise InvalidUser - def save(self): + async def async_save(self): """Save data.""" - json.save_json(self.path, self._data) - - -def load_data(path): - """Load auth data.""" - return Data(path, json.load_json(path, None)) + await self._store.async_save(self._data) @auth.AUTH_PROVIDERS.register('homeassistant') @@ -121,12 +124,10 @@ class HassAuthProvider(auth.AuthProvider): async def async_validate_login(self, username, password): """Helper to validate a username and password.""" - def validate(): - """Validate creds.""" - data = self._auth_data() - data.validate_login(username, password) - - await self.hass.async_add_job(validate) + data = Data(self.hass) + await data.async_load() + await self.hass.async_add_executor_job( + data.validate_login, username, password) async def async_get_or_create_credentials(self, flow_result): """Get credentials based on the flow result.""" @@ -141,10 +142,6 @@ class HassAuthProvider(auth.AuthProvider): 'username': username }) - def _auth_data(self): - """Return the auth provider data.""" - return load_data(self.hass.config.path(PATH_DATA)) - class LoginFlow(data_entry_flow.FlowHandler): """Handler for the login flow.""" diff --git a/homeassistant/scripts/auth.py b/homeassistant/scripts/auth.py index b4f1ddd2f11..dacdc7b18e2 100644 --- a/homeassistant/scripts/auth.py +++ b/homeassistant/scripts/auth.py @@ -1,7 +1,9 @@ """Script to manage users for the Home Assistant auth provider.""" import argparse +import asyncio import os +from homeassistant.core import HomeAssistant from homeassistant.config import get_default_config_dir from homeassistant.auth_providers import homeassistant as hass_auth @@ -17,7 +19,8 @@ def run(args): default=get_default_config_dir(), help="Directory that contains the Home Assistant configuration") - subparsers = parser.add_subparsers() + subparsers = parser.add_subparsers(dest='func') + subparsers.required = True parser_list = subparsers.add_parser('list') parser_list.set_defaults(func=list_users) @@ -37,11 +40,15 @@ def run(args): parser_change_pw.set_defaults(func=change_password) args = parser.parse_args(args) - path = os.path.join(os.getcwd(), args.config, hass_auth.PATH_DATA) - args.func(hass_auth.load_data(path), args) + loop = asyncio.get_event_loop() + hass = HomeAssistant(loop=loop) + hass.config.config_dir = os.path.join(os.getcwd(), args.config) + data = hass_auth.Data(hass) + loop.run_until_complete(data.async_load()) + loop.run_until_complete(args.func(data, args)) -def list_users(data, args): +async def list_users(data, args): """List the users.""" count = 0 for user in data.users: @@ -52,14 +59,14 @@ def list_users(data, args): print("Total users:", count) -def add_user(data, args): +async def add_user(data, args): """Create a user.""" data.add_user(args.username, args.password) - data.save() + await data.async_save() print("User created") -def validate_login(data, args): +async def validate_login(data, args): """Validate a login.""" try: data.validate_login(args.username, args.password) @@ -68,11 +75,11 @@ def validate_login(data, args): print("Auth invalid") -def change_password(data, args): +async def change_password(data, args): """Change password.""" try: data.change_password(args.username, args.new_password) - data.save() + await data.async_save() print("Password changed") except hass_auth.InvalidUser: print("User not found") diff --git a/tests/auth_providers/test_homeassistant.py b/tests/auth_providers/test_homeassistant.py index 8b12e682865..1d9a29bf48b 100644 --- a/tests/auth_providers/test_homeassistant.py +++ b/tests/auth_providers/test_homeassistant.py @@ -1,60 +1,48 @@ """Test the Home Assistant local auth provider.""" -from unittest.mock import patch, mock_open - import pytest from homeassistant import data_entry_flow from homeassistant.auth_providers import homeassistant as hass_auth -MOCK_PATH = '/bla/users.json' -JSON__OPEN_PATH = 'homeassistant.util.json.open' +@pytest.fixture +def data(hass): + """Create a loaded data class.""" + data = hass_auth.Data(hass) + hass.loop.run_until_complete(data.async_load()) + return data -def test_initialize_empty_config_file_not_found(): - """Test that we initialize an empty config.""" - with patch('homeassistant.util.json.open', side_effect=FileNotFoundError): - data = hass_auth.load_data(MOCK_PATH) - - assert data is not None - - -def test_adding_user(): +async def test_adding_user(data, hass): """Test adding a user.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') data.validate_login('test-user', 'test-pass') -def test_adding_user_duplicate_username(): +async def test_adding_user_duplicate_username(data, hass): """Test adding a user.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidUser): data.add_user('test-user', 'other-pass') -def test_validating_password_invalid_user(): +async def test_validating_password_invalid_user(data, hass): """Test validating an invalid user.""" - data = hass_auth.Data(MOCK_PATH, None) - with pytest.raises(hass_auth.InvalidAuth): data.validate_login('non-existing', 'pw') -def test_validating_password_invalid_password(): +async def test_validating_password_invalid_password(data, hass): """Test validating an invalid user.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidAuth): data.validate_login('test-user', 'invalid-pass') -def test_changing_password(): +async def test_changing_password(data, hass): """Test adding a user.""" user = 'test-user' - data = hass_auth.Data(MOCK_PATH, None) data.add_user(user, 'test-pass') data.change_password(user, 'new-pass') @@ -64,61 +52,50 @@ def test_changing_password(): data.validate_login(user, 'new-pass') -def test_changing_password_raises_invalid_user(): +async def test_changing_password_raises_invalid_user(data, hass): """Test that we initialize an empty config.""" - data = hass_auth.Data(MOCK_PATH, None) - with pytest.raises(hass_auth.InvalidUser): data.change_password('non-existing', 'pw') -async def test_login_flow_validates(hass): +async def test_login_flow_validates(data, hass): """Test login flow.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') + await data.async_save() provider = hass_auth.HassAuthProvider(hass, None, {}) flow = hass_auth.LoginFlow(provider) result = await flow.async_step_init() assert result['type'] == data_entry_flow.RESULT_TYPE_FORM - with patch.object(provider, '_auth_data', return_value=data): - result = await flow.async_step_init({ - 'username': 'incorrect-user', - 'password': 'test-pass', - }) - assert result['type'] == data_entry_flow.RESULT_TYPE_FORM - assert result['errors']['base'] == 'invalid_auth' + result = await flow.async_step_init({ + 'username': 'incorrect-user', + 'password': 'test-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['errors']['base'] == 'invalid_auth' - result = await flow.async_step_init({ - 'username': 'test-user', - 'password': 'incorrect-pass', - }) - assert result['type'] == data_entry_flow.RESULT_TYPE_FORM - assert result['errors']['base'] == 'invalid_auth' + result = await flow.async_step_init({ + 'username': 'test-user', + 'password': 'incorrect-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['errors']['base'] == 'invalid_auth' - result = await flow.async_step_init({ - 'username': 'test-user', - 'password': 'test-pass', - }) - assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + result = await flow.async_step_init({ + 'username': 'test-user', + 'password': 'test-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY -async def test_saving_loading(hass): +async def test_saving_loading(data, hass): """Test saving and loading JSON.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') data.add_user('second-user', 'second-pass') + await data.async_save() - with patch(JSON__OPEN_PATH, mock_open(), create=True) as mock_write: - await hass.async_add_job(data.save) - - # Mock open calls are: open file, context enter, write, context leave - written = mock_write.mock_calls[2][1][0] - - with patch('os.path.isfile', return_value=True), \ - patch(JSON__OPEN_PATH, mock_open(read_data=written), create=True): - await hass.async_add_job(hass_auth.load_data, MOCK_PATH) - + data = hass_auth.Data(hass) + await data.async_load() data.validate_login('test-user', 'test-pass') data.validate_login('second-user', 'second-pass') diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index 2e837b06b58..e6aa7893f33 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -6,16 +6,21 @@ import pytest from homeassistant.scripts import auth as script_auth from homeassistant.auth_providers import homeassistant as hass_auth -MOCK_PATH = '/bla/users.json' + +@pytest.fixture +def data(hass): + """Create a loaded data class.""" + data = hass_auth.Data(hass) + hass.loop.run_until_complete(data.async_load()) + return data -def test_list_user(capsys): +async def test_list_user(data, capsys): """Test we can list users.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') data.add_user('second-user', 'second-pass') - script_auth.list_users(data, None) + await script_auth.list_users(data, None) captured = capsys.readouterr() @@ -28,15 +33,12 @@ def test_list_user(capsys): ]) -def test_add_user(capsys): +async def test_add_user(data, capsys, hass_storage): """Test we can add a user.""" - data = hass_auth.Data(MOCK_PATH, None) + await script_auth.add_user( + data, Mock(username='paulus', password='test-pass')) - with patch.object(data, 'save') as mock_save: - script_auth.add_user( - data, Mock(username='paulus', password='test-pass')) - - assert len(mock_save.mock_calls) == 1 + assert len(hass_storage[hass_auth.STORAGE_KEY]['data']['users']) == 1 captured = capsys.readouterr() assert captured.out == 'User created\n' @@ -45,37 +47,34 @@ def test_add_user(capsys): data.validate_login('paulus', 'test-pass') -def test_validate_login(capsys): +async def test_validate_login(data, capsys): """Test we can validate a user login.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') - script_auth.validate_login( + await script_auth.validate_login( data, Mock(username='test-user', password='test-pass')) captured = capsys.readouterr() assert captured.out == 'Auth valid\n' - script_auth.validate_login( + await script_auth.validate_login( data, Mock(username='test-user', password='invalid-pass')) captured = capsys.readouterr() assert captured.out == 'Auth invalid\n' - script_auth.validate_login( + await script_auth.validate_login( data, Mock(username='invalid-user', password='test-pass')) captured = capsys.readouterr() assert captured.out == 'Auth invalid\n' -def test_change_password(capsys): +async def test_change_password(data, capsys, hass_storage): """Test we can change a password.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') - with patch.object(data, 'save') as mock_save: - script_auth.change_password( - data, Mock(username='test-user', new_password='new-pass')) + await script_auth.change_password( + data, Mock(username='test-user', new_password='new-pass')) - assert len(mock_save.mock_calls) == 1 + assert len(hass_storage[hass_auth.STORAGE_KEY]['data']['users']) == 1 captured = capsys.readouterr() assert captured.out == 'Password changed\n' data.validate_login('test-user', 'new-pass') @@ -83,18 +82,35 @@ def test_change_password(capsys): data.validate_login('test-user', 'test-pass') -def test_change_password_invalid_user(capsys): +async def test_change_password_invalid_user(data, capsys, hass_storage): """Test changing password of non-existing user.""" - data = hass_auth.Data(MOCK_PATH, None) data.add_user('test-user', 'test-pass') - with patch.object(data, 'save') as mock_save: - script_auth.change_password( - data, Mock(username='invalid-user', new_password='new-pass')) + await script_auth.change_password( + data, Mock(username='invalid-user', new_password='new-pass')) - assert len(mock_save.mock_calls) == 0 + assert hass_auth.STORAGE_KEY not in hass_storage captured = capsys.readouterr() assert captured.out == 'User not found\n' data.validate_login('test-user', 'test-pass') with pytest.raises(hass_auth.InvalidAuth): data.validate_login('invalid-user', 'new-pass') + + +def test_parsing_args(loop): + """Test we parse args correctly.""" + called = False + + async def mock_func(data, args2): + """Mock function to be called.""" + nonlocal called + called = True + assert data.hass.config.config_dir == '/somewhere/config' + assert args2 is args + + args = Mock(config='/somewhere/config', func=mock_func) + + with patch('argparse.ArgumentParser.parse_args', return_value=args): + script_auth.run(None) + + assert called, 'Mock function did not get called' From e3e014bccca3e3f505eabaf7574ee495d5ed05a7 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 29 Jun 2018 16:09:46 +0200 Subject: [PATCH 098/128] Fix zwave climate operation mode mappings (#15162) --- homeassistant/components/climate/zwave.py | 33 ++++++++++++--- tests/components/climate/test_zwave.py | 49 ++++++++++++++++++++++- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index c87d1507e92..52c544256b6 100644 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -7,12 +7,13 @@ https://home-assistant.io/components/climate.zwave/ # Because we do not compile openzwave on CI import logging from homeassistant.components.climate import ( - DOMAIN, ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, + DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE) from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) + STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) _LOGGER = logging.getLogger(__name__) @@ -31,6 +32,15 @@ DEVICE_MAPPINGS = { REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120 } +STATE_MAPPINGS = { + 'Off': STATE_OFF, + 'Heat': STATE_HEAT, + 'Heat Mode': STATE_HEAT, + 'Heat (Default)': STATE_HEAT, + 'Cool': STATE_COOL, + 'Auto': STATE_AUTO, +} + def get_device(hass, values, **kwargs): """Create Z-Wave entity device.""" @@ -48,6 +58,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._current_temperature = None self._current_operation = None self._operation_list = None + self._operation_mapping = None self._operating_state = None self._current_fan_mode = None self._fan_list = None @@ -86,10 +97,21 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """Handle the data changes for node values.""" # Operation Mode if self.values.mode: - self._current_operation = self.values.mode.data + self._operation_list = [] + self._operation_mapping = {} operation_list = self.values.mode.data_items if operation_list: - self._operation_list = list(operation_list) + for mode in operation_list: + ha_mode = STATE_MAPPINGS.get(mode) + if ha_mode and ha_mode not in self._operation_mapping: + self._operation_mapping[ha_mode] = mode + self._operation_list.append(ha_mode) + continue + self._operation_list.append(mode) + current_mode = self.values.mode.data + self._current_operation = next( + (key for key, value in self._operation_mapping.items() + if value == current_mode), current_mode) _LOGGER.debug("self._operation_list=%s", self._operation_list) _LOGGER.debug("self._current_operation=%s", self._current_operation) @@ -205,7 +227,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): def set_operation_mode(self, operation_mode): """Set new target operation mode.""" if self.values.mode: - self.values.mode.data = operation_mode + self.values.mode.data = self._operation_mapping.get( + operation_mode, operation_mode) def set_swing_mode(self, swing_mode): """Set new target swing mode.""" diff --git a/tests/components/climate/test_zwave.py b/tests/components/climate/test_zwave.py index fbd6ea7f798..39a85ab493f 100644 --- a/tests/components/climate/test_zwave.py +++ b/tests/components/climate/test_zwave.py @@ -1,9 +1,9 @@ """Test Z-Wave climate devices.""" import pytest -from homeassistant.components.climate import zwave +from homeassistant.components.climate import zwave, STATE_COOL, STATE_HEAT from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) + STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) from tests.mock.zwave import ( MockNode, MockValue, MockEntityValues, value_changed) @@ -46,6 +46,24 @@ def device_zxt_120(hass, mock_openzwave): yield device +@pytest.fixture +def device_mapping(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Test state mapping.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), + mode=MockValue(data='Off', data_items=['Off', 'Cool', 'Heat'], + node=node), + fan_mode=MockValue(data='test2', data_items=[3, 4, 5], node=node), + operating_state=MockValue(data=6, node=node), + fan_state=MockValue(data=7, node=node), + ) + device = zwave.get_device(hass, node=node, values=values, node_config={}) + + yield device + + def test_zxt_120_swing_mode(device_zxt_120): """Test operation of the zxt 120 swing mode.""" device = device_zxt_120 @@ -109,6 +127,18 @@ def test_operation_value_set(device): assert device.values.mode.data == 'test_set' +def test_operation_value_set_mapping(device_mapping): + """Test values changed for climate device. Mapping.""" + device = device_mapping + assert device.values.mode.data == 'Off' + device.set_operation_mode(STATE_HEAT) + assert device.values.mode.data == 'Heat' + device.set_operation_mode(STATE_COOL) + assert device.values.mode.data == 'Cool' + device.set_operation_mode(STATE_OFF) + assert device.values.mode.data == 'Off' + + def test_fan_mode_value_set(device): """Test values changed for climate device.""" assert device.values.fan_mode.data == 'test2' @@ -140,6 +170,21 @@ def test_operation_value_changed(device): assert device.current_operation == 'test_updated' +def test_operation_value_changed_mapping(device_mapping): + """Test values changed for climate device. Mapping.""" + device = device_mapping + assert device.current_operation == 'off' + device.values.mode.data = 'Heat' + value_changed(device.values.mode) + assert device.current_operation == STATE_HEAT + device.values.mode.data = 'Cool' + value_changed(device.values.mode) + assert device.current_operation == STATE_COOL + device.values.mode.data = 'Off' + value_changed(device.values.mode) + assert device.current_operation == STATE_OFF + + def test_fan_mode_value_changed(device): """Test values changed for climate device.""" assert device.current_fan_mode == 'test2' From c61a652c9085efc0717682de432488fe0975c982 Mon Sep 17 00:00:00 2001 From: Sriram Vaidyanathan Date: Fri, 29 Jun 2018 19:53:14 +0530 Subject: [PATCH 099/128] Fixed Indentation error (#15210) * Fixed Indentation error * Update xiaomi.py --- homeassistant/components/camera/xiaomi.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/xiaomi.py b/homeassistant/components/camera/xiaomi.py index f0e66dbd20e..e80f4b7532a 100644 --- a/homeassistant/components/camera/xiaomi.py +++ b/homeassistant/components/camera/xiaomi.py @@ -113,14 +113,16 @@ class XiaomiCamera(Camera): except error_perm as exc: _LOGGER.error('Unable to find path: %s - %s', first_dir, exc) return False + if self._model == MODEL_XIAOFANG: dirs = [d for d in ftp.nlst() if '.' not in d] if not dirs: _LOGGER.warning("There don't appear to be any uploaded videos") return False - latest_dir = dirs[-1] - ftp.cwd(latest_dir) + latest_dir = dirs[-1] + ftp.cwd(latest_dir) + videos = [v for v in ftp.nlst() if '.tmp' not in v] if not videos: _LOGGER.info('Video folder "%s" is empty; delaying', latest_dir) From fd38caa287b500da9e6efbd2e2034984356e99e6 Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Fri, 29 Jun 2018 16:27:06 -0400 Subject: [PATCH 100/128] X-Forwarded-For improvements and bug fixes (#15204) * Use new trusted_proxies setting for X-Forwarded-For whitelist * Only use the last IP in the header Per Wikipedia (https://en.wikipedia.org/wiki/X-Forwarded-For#Format): > The last IP address is always the IP address that connects to the last proxy, > which means it is the most reliable source of information. * Add two additional tests * Ignore nonsense header values instead of failing --- .../components/emulated_hue/__init__.py | 1 + homeassistant/components/http/__init__.py | 9 +++- homeassistant/components/http/real_ip.py | 17 ++++--- tests/components/http/test_real_ip.py | 48 +++++++++++++++++++ tests/scripts/test_check_config.py | 1 + 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 708b3db83cd..6988e20fb5f 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -95,6 +95,7 @@ def setup(hass, yaml_config): ssl_key=None, cors_origins=None, use_x_forwarded_for=False, + trusted_proxies=[], trusted_networks=[], login_threshold=0, is_ban_enabled=False diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index f769d2bc4ff..9d43a741ba5 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -44,6 +44,7 @@ CONF_SSL_PEER_CERTIFICATE = 'ssl_peer_certificate' CONF_SSL_KEY = 'ssl_key' CONF_CORS_ORIGINS = 'cors_allowed_origins' CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' +CONF_TRUSTED_PROXIES = 'trusted_proxies' CONF_TRUSTED_NETWORKS = 'trusted_networks' CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold' CONF_IP_BAN_ENABLED = 'ip_ban_enabled' @@ -86,6 +87,8 @@ HTTP_SCHEMA = vol.Schema({ vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean, + vol.Optional(CONF_TRUSTED_PROXIES, default=[]): + vol.All(cv.ensure_list, [ip_network]), vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): vol.All(cv.ensure_list, [ip_network]), vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD, @@ -114,6 +117,7 @@ async def async_setup(hass, config): ssl_key = conf.get(CONF_SSL_KEY) cors_origins = conf[CONF_CORS_ORIGINS] use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR] + trusted_proxies = conf[CONF_TRUSTED_PROXIES] trusted_networks = conf[CONF_TRUSTED_NETWORKS] is_ban_enabled = conf[CONF_IP_BAN_ENABLED] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] @@ -132,6 +136,7 @@ async def async_setup(hass, config): ssl_key=ssl_key, cors_origins=cors_origins, use_x_forwarded_for=use_x_forwarded_for, + trusted_proxies=trusted_proxies, trusted_networks=trusted_networks, login_threshold=login_threshold, is_ban_enabled=is_ban_enabled @@ -173,14 +178,14 @@ class HomeAssistantHTTP(object): def __init__(self, hass, api_password, ssl_certificate, ssl_peer_certificate, ssl_key, server_host, server_port, cors_origins, - use_x_forwarded_for, trusted_networks, + use_x_forwarded_for, trusted_proxies, trusted_networks, login_threshold, is_ban_enabled): """Initialize the HTTP Home Assistant server.""" app = self.app = web.Application( middlewares=[staticresource_middleware]) # This order matters - setup_real_ip(app, use_x_forwarded_for, trusted_networks) + setup_real_ip(app, use_x_forwarded_for, trusted_proxies) if is_ban_enabled: setup_bans(hass, app, login_threshold) diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py index 401a09dc306..f8adc815fde 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -11,7 +11,7 @@ from .const import KEY_REAL_IP @callback -def setup_real_ip(app, use_x_forwarded_for, trusted_networks): +def setup_real_ip(app, use_x_forwarded_for, trusted_proxies): """Create IP Ban middleware for the app.""" @middleware async def real_ip_middleware(request, handler): @@ -21,12 +21,15 @@ def setup_real_ip(app, use_x_forwarded_for, trusted_networks): request[KEY_REAL_IP] = connected_ip # Only use the XFF header if enabled, present, and from a trusted proxy - if (use_x_forwarded_for and - X_FORWARDED_FOR in request.headers and - any(connected_ip in trusted_network - for trusted_network in trusted_networks)): - request[KEY_REAL_IP] = ip_address( - request.headers.get(X_FORWARDED_FOR).split(',')[0]) + try: + if (use_x_forwarded_for and + X_FORWARDED_FOR in request.headers and + any(connected_ip in trusted_proxy + for trusted_proxy in trusted_proxies)): + request[KEY_REAL_IP] = ip_address( + request.headers.get(X_FORWARDED_FOR).split(', ')[-1]) + except ValueError: + pass return await handler(request) diff --git a/tests/components/http/test_real_ip.py b/tests/components/http/test_real_ip.py index b6af8159207..6cf6fec6bce 100644 --- a/tests/components/http/test_real_ip.py +++ b/tests/components/http/test_real_ip.py @@ -58,3 +58,51 @@ async def test_use_x_forwarded_for_with_trusted_proxy(aiohttp_client): assert resp.status == 200 text = await resp.text() assert text == '255.255.255.255' + + +async def test_use_x_forwarded_for_with_untrusted_proxy(aiohttp_client): + """Test that we get the IP from the transport.""" + app = web.Application() + app.router.add_get('/', mock_handler) + setup_real_ip(app, True, [ip_network('1.1.1.1')]) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get('/', headers={ + X_FORWARDED_FOR: '255.255.255.255' + }) + assert resp.status == 200 + text = await resp.text() + assert text != '255.255.255.255' + + +async def test_use_x_forwarded_for_with_spoofed_header(aiohttp_client): + """Test that we get the IP from the transport.""" + app = web.Application() + app.router.add_get('/', mock_handler) + setup_real_ip(app, True, [ip_network('127.0.0.1')]) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get('/', headers={ + X_FORWARDED_FOR: '222.222.222.222, 255.255.255.255' + }) + assert resp.status == 200 + text = await resp.text() + assert text == '255.255.255.255' + + +async def test_use_x_forwarded_for_with_nonsense_header(aiohttp_client): + """Test that we get the IP from the transport.""" + app = web.Application() + app.router.add_get('/', mock_handler) + setup_real_ip(app, True, [ip_network('127.0.0.1')]) + + mock_api_client = await aiohttp_client(app) + + resp = await mock_api_client.get('/', headers={ + X_FORWARDED_FOR: 'This value is invalid' + }) + assert resp.status == 200 + text = await resp.text() + assert text == '127.0.0.1' diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 8dfc5db90e0..33154090286 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -160,6 +160,7 @@ class TestCheckConfig(unittest.TestCase): 'server_host': '0.0.0.0', 'server_port': 8123, 'trusted_networks': [], + 'trusted_proxies': [], 'use_x_forwarded_for': False} assert res['secret_cache'] == {secrets_path: {'http_pw': 'abc123'}} assert res['secrets'] == {'http_pw': 'abc123'} From 94b55efef3bf1728eb2dcdec9457d4d9beef7f4a Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Fri, 29 Jun 2018 23:18:44 +0200 Subject: [PATCH 101/128] Stop supporting deprecated TLS ciphers (#15217) * Stop supporting deprecated TLS ciphers * Lint --- homeassistant/components/http/__init__.py | 24 +++++++++-------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 9d43a741ba5..485433434fd 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -51,24 +51,18 @@ CONF_IP_BAN_ENABLED = 'ip_ban_enabled' # TLS configuration follows the best-practice guidelines specified here: # https://wiki.mozilla.org/Security/Server_Side_TLS -# Intermediate guidelines are followed. -SSL_VERSION = ssl.PROTOCOL_SSLv23 -SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 +# Modern guidelines are followed. +SSL_VERSION = ssl.PROTOCOL_TLS # pylint: disable=no-member +SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | \ + ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | \ + ssl.OP_CIPHER_SERVER_PREFERENCE if hasattr(ssl, 'OP_NO_COMPRESSION'): SSL_OPTS |= ssl.OP_NO_COMPRESSION -CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ +CIPHERS = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ + "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \ - "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ - "DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" \ - "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:" \ - "ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:" \ - "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:" \ - "ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:" \ - "DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:" \ - "DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:" \ - "ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:" \ - "AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \ - "AES256-SHA:DES-CBC3-SHA:!DSS" + "ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:" \ + "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" _LOGGER = logging.getLogger(__name__) From bbbec5a0565e9793d7880c2f30ec630176bcc6e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jun 2018 17:21:50 -0400 Subject: [PATCH 102/128] Bump frontend to 20180629.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 0e9d7612669..84118e57c8f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180627.0'] +REQUIREMENTS = ['home-assistant-frontend==20180629.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 1a3b6cacf9e..862a6d3f894 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180627.0 +home-assistant-frontend==20180629.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ae0a5db6c3..7bb05bbdd00 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==20180627.0 +home-assistant-frontend==20180629.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 66479dc2e55469db34bb0f918d0a815e53548afa Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 29 Jun 2018 23:25:49 +0200 Subject: [PATCH 103/128] Update eternalegypt (#15180) * Update eternalegypt to 0.0.2 * Share websession * Renames --- homeassistant/components/netgear_lte.py | 56 ++++++++++++------- .../components/notify/netgear_lte.py | 8 +-- .../components/sensor/netgear_lte.py | 14 ++--- requirements_all.txt | 2 +- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/netgear_lte.py b/homeassistant/components/netgear_lte.py index 4887ea1aa67..23a01d37c2b 100644 --- a/homeassistant/components/netgear_lte.py +++ b/homeassistant/components/netgear_lte.py @@ -9,12 +9,15 @@ from datetime import timedelta import voluptuous as vol import attr +import aiohttp -from homeassistant.const import CONF_HOST, CONF_PASSWORD -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.util import Throttle -REQUIREMENTS = ['eternalegypt==0.0.1'] +REQUIREMENTS = ['eternalegypt==0.0.2'] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) @@ -30,33 +33,34 @@ CONFIG_SCHEMA = vol.Schema({ @attr.s -class LTEData: - """Class for LTE state.""" +class ModemData: + """Class for modem state.""" - eternalegypt = attr.ib() + modem = attr.ib() unread_count = attr.ib(init=False) usage = attr.ib(init=False) @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Call the API to update the data.""" - information = await self.eternalegypt.information() + information = await self.modem.information() self.unread_count = sum(1 for x in information.sms if x.unread) self.usage = information.usage @attr.s -class LTEHostData: - """Container for LTE states.""" +class LTEData: + """Shared state.""" - hostdata = attr.ib(init=False, factory=dict) + websession = attr.ib() + modem_data = attr.ib(init=False, factory=dict) - def get(self, config): - """Get the requested or the only hostdata value.""" + def get_modem_data(self, config): + """Get the requested or the only modem_data value.""" if CONF_HOST in config: - return self.hostdata.get(config[CONF_HOST]) - elif len(self.hostdata) == 1: - return next(iter(self.hostdata.values())) + return self.modem_data.get(config[CONF_HOST]) + elif len(self.modem_data) == 1: + return next(iter(self.modem_data.values())) return None @@ -64,7 +68,9 @@ class LTEHostData: async def async_setup(hass, config): """Set up Netgear LTE component.""" if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = LTEHostData() + websession = async_create_clientsession( + hass, cookie_jar=aiohttp.CookieJar(unsafe=True)) + hass.data[DATA_KEY] = LTEData(websession) tasks = [_setup_lte(hass, conf) for conf in config.get(DOMAIN, [])] if tasks: @@ -80,7 +86,17 @@ async def _setup_lte(hass, lte_config): host = lte_config[CONF_HOST] password = lte_config[CONF_PASSWORD] - eternalegypt = eternalegypt.LB2120(host, password) - lte_data = LTEData(eternalegypt) - await lte_data.async_update() - hass.data[DATA_KEY].hostdata[host] = lte_data + websession = hass.data[DATA_KEY].websession + + modem = eternalegypt.Modem(hostname=host, websession=websession) + await modem.login(password=password) + + modem_data = ModemData(modem) + await modem_data.async_update() + hass.data[DATA_KEY].modem_data[host] = modem_data + + async def cleanup(event): + """Clean up resources.""" + await modem.logout() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) diff --git a/homeassistant/components/notify/netgear_lte.py b/homeassistant/components/notify/netgear_lte.py index b4ed53b828d..97dfe504a51 100644 --- a/homeassistant/components/notify/netgear_lte.py +++ b/homeassistant/components/notify/netgear_lte.py @@ -25,16 +25,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ async def async_get_service(hass, config, discovery_info=None): """Get the notification service.""" - lte_data = hass.data[DATA_KEY].get(config) + modem_data = hass.data[DATA_KEY].get_modem_data(config) phone = config.get(ATTR_TARGET) - return NetgearNotifyService(lte_data, phone) + return NetgearNotifyService(modem_data, phone) @attr.s class NetgearNotifyService(BaseNotificationService): """Implementation of a notification service.""" - lte_data = attr.ib() + modem_data = attr.ib() phone = attr.ib() async def async_send_message(self, message="", **kwargs): @@ -42,4 +42,4 @@ class NetgearNotifyService(BaseNotificationService): targets = kwargs.get(ATTR_TARGET, self.phone) if targets and message: for target in targets: - await self.lte_data.eternalegypt.sms(target, message) + await self.modem_data.modem.sms(target, message) diff --git a/homeassistant/components/sensor/netgear_lte.py b/homeassistant/components/sensor/netgear_lte.py index 859435edbc9..b4a3e2a1155 100644 --- a/homeassistant/components/sensor/netgear_lte.py +++ b/homeassistant/components/sensor/netgear_lte.py @@ -29,14 +29,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ async def async_setup_platform( hass, config, async_add_devices, discovery_info): """Set up Netgear LTE sensor devices.""" - lte_data = hass.data[DATA_KEY].get(config) + modem_data = hass.data[DATA_KEY].get_modem_data(config) sensors = [] for sensortype in config[CONF_SENSORS]: if sensortype == SENSOR_SMS: - sensors.append(SMSSensor(lte_data)) + sensors.append(SMSSensor(modem_data)) elif sensortype == SENSOR_USAGE: - sensors.append(UsageSensor(lte_data)) + sensors.append(UsageSensor(modem_data)) async_add_devices(sensors, True) @@ -45,11 +45,11 @@ async def async_setup_platform( class LTESensor(Entity): """Data usage sensor entity.""" - lte_data = attr.ib() + modem_data = attr.ib() async def async_update(self): """Update state.""" - await self.lte_data.async_update() + await self.modem_data.async_update() class SMSSensor(LTESensor): @@ -63,7 +63,7 @@ class SMSSensor(LTESensor): @property def state(self): """Return the state of the sensor.""" - return self.lte_data.unread_count + return self.modem_data.unread_count class UsageSensor(LTESensor): @@ -82,4 +82,4 @@ class UsageSensor(LTESensor): @property def state(self): """Return the state of the sensor.""" - return round(self.lte_data.usage / 1024**2, 1) + return round(self.modem_data.usage / 1024**2, 1) diff --git a/requirements_all.txt b/requirements_all.txt index 862a6d3f894..4b795a9b39b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -309,7 +309,7 @@ ephem==3.7.6.0 epson-projector==0.1.3 # homeassistant.components.netgear_lte -eternalegypt==0.0.1 +eternalegypt==0.0.2 # homeassistant.components.keyboard_remote # evdev==0.6.1 From 49623d2dadd05615696a3319b98ae9fae19143e0 Mon Sep 17 00:00:00 2001 From: Vignesh Venkat Date: Fri, 29 Jun 2018 14:26:06 -0700 Subject: [PATCH 104/128] Update python-wink to 1.9.1 (#15215) Fixes a bug for when GE Z-Wave fan speeds are adjusted using the wink app. --- homeassistant/components/wink/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 7016250c6b1..7c171d74967 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import track_time_interval from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['python-wink==1.9.0', 'pubnubsub-handler==1.0.2'] +REQUIREMENTS = ['python-wink==1.9.1', 'pubnubsub-handler==1.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 4b795a9b39b..1267a702811 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1098,7 +1098,7 @@ python-velbus==2.0.11 python-vlc==1.1.2 # homeassistant.components.wink -python-wink==1.9.0 +python-wink==1.9.1 # homeassistant.components.sensor.swiss_public_transport python_opendata_transport==0.1.3 From 56f17b8651dc7867019de7295bd2ef47a6d555b6 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 29 Jun 2018 23:26:48 +0200 Subject: [PATCH 105/128] =?UTF-8?q?Fix=20'AirQualityMonitorStatus'=20objec?= =?UTF-8?q?t=20has=20no=20attribute=20=E2=80=98time=5Fstate=E2=80=99=20(#1?= =?UTF-8?q?5216)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/sensor/xiaomi_miio.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index a70d701fac6..63d93d31cf3 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -30,7 +30,11 @@ REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_CHARGING = 'charging' ATTR_BATTERY_LEVEL = 'battery_level' -ATTR_TIME_STATE = 'time_state' +ATTR_DISPLAY_CLOCK = 'display_clock' +ATTR_NIGHT_MODE = 'night_mode' +ATTR_NIGHT_TIME_BEGIN = 'night_time_begin' +ATTR_NIGHT_TIME_END = 'night_time_end' +ATTR_SENSOR_STATE = 'sensor_state' ATTR_MODEL = 'model' SUCCESS = ['ok'] @@ -85,7 +89,11 @@ class XiaomiAirQualityMonitor(Entity): ATTR_POWER: None, ATTR_BATTERY_LEVEL: None, ATTR_CHARGING: None, - ATTR_TIME_STATE: None, + ATTR_DISPLAY_CLOCK: None, + ATTR_NIGHT_MODE: None, + ATTR_NIGHT_TIME_BEGIN: None, + ATTR_NIGHT_TIME_END: None, + ATTR_SENSOR_STATE: None, ATTR_MODEL: self._model, } @@ -143,7 +151,11 @@ class XiaomiAirQualityMonitor(Entity): ATTR_POWER: state.power, ATTR_CHARGING: state.usb_power, ATTR_BATTERY_LEVEL: state.battery, - ATTR_TIME_STATE: state.time_state, + ATTR_DISPLAY_CLOCK: state.display_clock, + ATTR_NIGHT_MODE: state.night_mode, + ATTR_NIGHT_TIME_BEGIN: state.night_time_begin, + ATTR_NIGHT_TIME_END: state.night_time_end, + ATTR_SENSOR_STATE: state.sensor_state, }) except DeviceException as ex: From 2524dca7bfc006b9dd1b4aaaf9b90e2b2d7a1ad7 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Fri, 29 Jun 2018 14:27:18 -0700 Subject: [PATCH 106/128] Use cached states for neato when possible (#15218) --- homeassistant/components/switch/neato.py | 2 +- homeassistant/components/vacuum/neato.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py index dca5d63b43d..34dad9bb581 100644 --- a/homeassistant/components/switch/neato.py +++ b/homeassistant/components/switch/neato.py @@ -67,7 +67,7 @@ class NeatoConnectedSwitch(ToggleEntity): _LOGGER.debug('self._state=%s', self._state) if self.type == SWITCH_TYPE_SCHEDULE: _LOGGER.debug("State: %s", self._state) - if self.robot.schedule_enabled: + if self._state['details']['isScheduleEnabled']: self._schedule_state = STATE_ON else: self._schedule_state = STATE_OFF diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 1b32fff9e5b..6289fed265d 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -96,14 +96,14 @@ class NeatoConnectedVacuum(VacuumDevice): elif self._state['state'] == 4: self._status_state = ERRORS.get(self._state['error']) - if (self.robot.state['action'] == 1 or - self.robot.state['action'] == 2 or - self.robot.state['action'] == 3 and - self.robot.state['state'] == 2): + if (self._state['action'] == 1 or + self._state['action'] == 2 or + self._state['action'] == 3 and + self._state['state'] == 2): self._clean_state = STATE_ON - elif (self.robot.state['action'] == 11 or - self.robot.state['action'] == 12 and - self.robot.state['state'] == 2): + elif (self._state['action'] == 11 or + self._state['action'] == 12 and + self._state['state'] == 2): self._clean_state = STATE_ON else: self._clean_state = STATE_OFF From fa79aead9ad7d3fb0598edc66dfeb91a00d28076 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jun 2018 18:03:42 -0400 Subject: [PATCH 107/128] Bumped version to 0.74.0b0 --- homeassistant/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cb6858639f4..5b100414e48 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 73 -PATCH_VERSION = '0.dev0' +MINOR_VERSION = 74 +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 56bbadb5018e98dc07268c2e95c3b73ff6eb8be4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jun 2018 18:06:32 -0400 Subject: [PATCH 108/128] Version bump to 0.73.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5b100414e48..8511941ce02 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 = 74 +MINOR_VERSION = 73 PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 5d6db9a915b084fbe4675183add0e5899aa87f1e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Jul 2018 13:00:34 -0400 Subject: [PATCH 109/128] Bump frontend to 20180701.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 84118e57c8f..7bad8ff727d 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180629.1'] +REQUIREMENTS = ['home-assistant-frontend==20180701.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 1267a702811..a7422b971b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180629.1 +home-assistant-frontend==20180701.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7bb05bbdd00..124fdc736a8 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==20180629.1 +home-assistant-frontend==20180701.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From c3ad30ec87cd73fbfc9c3df9ea6203db79436bec Mon Sep 17 00:00:00 2001 From: Andy Castille Date: Sun, 1 Jul 2018 10:54:51 -0500 Subject: [PATCH 110/128] Rachio webhooks (#15111) * Make fewer requests to the Rachio API * BREAKING: Rewrite Rachio component --- .../components/binary_sensor/rachio.py | 127 +++++++ homeassistant/components/rachio.py | 289 ++++++++++++++++ homeassistant/components/switch/rachio.py | 324 +++++++++--------- requirements_all.txt | 4 +- 4 files changed, 586 insertions(+), 158 deletions(-) create mode 100644 homeassistant/components/binary_sensor/rachio.py create mode 100644 homeassistant/components/rachio.py diff --git a/homeassistant/components/binary_sensor/rachio.py b/homeassistant/components/binary_sensor/rachio.py new file mode 100644 index 00000000000..cc3079c6e53 --- /dev/null +++ b/homeassistant/components/binary_sensor/rachio.py @@ -0,0 +1,127 @@ +""" +Integration with the Rachio Iro sprinkler system controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.rachio/ +""" +from abc import abstractmethod +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.rachio import (DOMAIN as DOMAIN_RACHIO, + KEY_DEVICE_ID, + KEY_STATUS, + KEY_SUBTYPE, + SIGNAL_RACHIO_CONTROLLER_UPDATE, + STATUS_OFFLINE, + STATUS_ONLINE, + SUBTYPE_OFFLINE, + SUBTYPE_ONLINE,) +from homeassistant.helpers.dispatcher import dispatcher_connect + +DEPENDENCIES = ['rachio'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Rachio binary sensors.""" + devices = [] + for controller in hass.data[DOMAIN_RACHIO].controllers: + devices.append(RachioControllerOnlineBinarySensor(hass, controller)) + + add_devices(devices) + _LOGGER.info("%d Rachio binary sensor(s) added", len(devices)) + + +class RachioControllerBinarySensor(BinarySensorDevice): + """Represent a binary sensor that reflects a Rachio state.""" + + def __init__(self, hass, controller, poll=True): + """Set up a new Rachio controller binary sensor.""" + self._controller = controller + + if poll: + self._state = self._poll_update() + else: + self._state = None + + dispatcher_connect(hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, + self._handle_any_update) + + @property + def should_poll(self) -> bool: + """Declare that this entity pushes its state to HA.""" + return False + + @property + def is_on(self) -> bool: + """Return whether the sensor has a 'true' value.""" + return self._state + + def _handle_any_update(self, *args, **kwargs) -> None: + """Determine whether an update event applies to this device.""" + if args[0][KEY_DEVICE_ID] != self._controller.controller_id: + # For another device + return + + # For this device + self._handle_update() + + @abstractmethod + def _poll_update(self, data=None) -> bool: + """Request the state from the API.""" + pass + + @abstractmethod + def _handle_update(self, *args, **kwargs) -> None: + """Handle an update to the state of this sensor.""" + pass + + +class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): + """Represent a binary sensor that reflects if the controller is online.""" + + def __init__(self, hass, controller): + """Set up a new Rachio controller online binary sensor.""" + super().__init__(hass, controller, poll=False) + self._state = self._poll_update(controller.init_data) + + @property + def name(self) -> str: + """Return the name of this sensor including the controller name.""" + return "{} online".format(self._controller.name) + + @property + def device_class(self) -> str: + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'connectivity' + + @property + def icon(self) -> str: + """Return the name of an icon for this sensor.""" + return 'mdi:wifi-strength-4' if self.is_on\ + else 'mdi:wifi-strength-off-outline' + + def _poll_update(self, data=None) -> bool: + """Request the state from the API.""" + if data is None: + data = self._controller.rachio.device.get( + self._controller.controller_id)[1] + + if data[KEY_STATUS] == STATUS_ONLINE: + return True + elif data[KEY_STATUS] == STATUS_OFFLINE: + return False + else: + _LOGGER.warning('"%s" reported in unknown state "%s"', self.name, + data[KEY_STATUS]) + + def _handle_update(self, *args, **kwargs) -> None: + """Handle an update to the state of this sensor.""" + if args[0][KEY_SUBTYPE] == SUBTYPE_ONLINE: + self._state = True + elif args[0][KEY_SUBTYPE] == SUBTYPE_OFFLINE: + self._state = False + + self.schedule_update_ha_state() diff --git a/homeassistant/components/rachio.py b/homeassistant/components/rachio.py new file mode 100644 index 00000000000..b3b2d05e933 --- /dev/null +++ b/homeassistant/components/rachio.py @@ -0,0 +1,289 @@ +""" +Integration with the Rachio Iro sprinkler system controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/rachio/ +""" +import asyncio +import logging + +from aiohttp import web +import voluptuous as vol + +from homeassistant.auth import generate_secret +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send + +REQUIREMENTS = ['rachiopy==0.1.3'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'rachio' + +CONF_CUSTOM_URL = 'hass_url_override' +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_CUSTOM_URL): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +# Keys used in the API JSON +KEY_DEVICE_ID = 'deviceId' +KEY_DEVICES = 'devices' +KEY_ENABLED = 'enabled' +KEY_EXTERNAL_ID = 'externalId' +KEY_ID = 'id' +KEY_NAME = 'name' +KEY_ON = 'on' +KEY_STATUS = 'status' +KEY_SUBTYPE = 'subType' +KEY_SUMMARY = 'summary' +KEY_TYPE = 'type' +KEY_URL = 'url' +KEY_USERNAME = 'username' +KEY_ZONE_ID = 'zoneId' +KEY_ZONE_NUMBER = 'zoneNumber' +KEY_ZONES = 'zones' + +STATUS_ONLINE = 'ONLINE' +STATUS_OFFLINE = 'OFFLINE' + +# Device webhook values +TYPE_CONTROLLER_STATUS = 'DEVICE_STATUS' +SUBTYPE_OFFLINE = 'OFFLINE' +SUBTYPE_ONLINE = 'ONLINE' +SUBTYPE_OFFLINE_NOTIFICATION = 'OFFLINE_NOTIFICATION' +SUBTYPE_COLD_REBOOT = 'COLD_REBOOT' +SUBTYPE_SLEEP_MODE_ON = 'SLEEP_MODE_ON' +SUBTYPE_SLEEP_MODE_OFF = 'SLEEP_MODE_OFF' +SUBTYPE_BROWNOUT_VALVE = 'BROWNOUT_VALVE' +SUBTYPE_RAIN_SENSOR_DETECTION_ON = 'RAIN_SENSOR_DETECTION_ON' +SUBTYPE_RAIN_SENSOR_DETECTION_OFF = 'RAIN_SENSOR_DETECTION_OFF' +SUBTYPE_RAIN_DELAY_ON = 'RAIN_DELAY_ON' +SUBTYPE_RAIN_DELAY_OFF = 'RAIN_DELAY_OFF' + +# Schedule webhook values +TYPE_SCHEDULE_STATUS = 'SCHEDULE_STATUS' +SUBTYPE_SCHEDULE_STARTED = 'SCHEDULE_STARTED' +SUBTYPE_SCHEDULE_STOPPED = 'SCHEDULE_STOPPED' +SUBTYPE_SCHEDULE_COMPLETED = 'SCHEDULE_COMPLETED' +SUBTYPE_WEATHER_NO_SKIP = 'WEATHER_INTELLIGENCE_NO_SKIP' +SUBTYPE_WEATHER_SKIP = 'WEATHER_INTELLIGENCE_SKIP' +SUBTYPE_WEATHER_CLIMATE_SKIP = 'WEATHER_INTELLIGENCE_CLIMATE_SKIP' +SUBTYPE_WEATHER_FREEZE = 'WEATHER_INTELLIGENCE_FREEZE' + +# Zone webhook values +TYPE_ZONE_STATUS = 'ZONE_STATUS' +SUBTYPE_ZONE_STARTED = 'ZONE_STARTED' +SUBTYPE_ZONE_STOPPED = 'ZONE_STOPPED' +SUBTYPE_ZONE_COMPLETED = 'ZONE_COMPLETED' +SUBTYPE_ZONE_CYCLING = 'ZONE_CYCLING' +SUBTYPE_ZONE_CYCLING_COMPLETED = 'ZONE_CYCLING_COMPLETED' + +# Webhook callbacks +LISTEN_EVENT_TYPES = ['DEVICE_STATUS_EVENT', 'ZONE_STATUS_EVENT'] +WEBHOOK_CONST_ID = 'homeassistant.rachio:' +WEBHOOK_PATH = URL_API + DOMAIN +SIGNAL_RACHIO_UPDATE = DOMAIN + '_update' +SIGNAL_RACHIO_CONTROLLER_UPDATE = SIGNAL_RACHIO_UPDATE + '_controller' +SIGNAL_RACHIO_ZONE_UPDATE = SIGNAL_RACHIO_UPDATE + '_zone' +SIGNAL_RACHIO_SCHEDULE_UPDATE = SIGNAL_RACHIO_UPDATE + '_schedule' + + +def setup(hass, config) -> bool: + """Set up the Rachio component.""" + from rachiopy import Rachio + + # Listen for incoming webhook connections + hass.http.register_view(RachioWebhookView()) + + # Configure API + api_key = config[DOMAIN].get(CONF_API_KEY) + rachio = Rachio(api_key) + + # Get the URL of this server + custom_url = config[DOMAIN].get(CONF_CUSTOM_URL) + hass_url = hass.config.api.base_url if custom_url is None else custom_url + rachio.webhook_auth = generate_secret() + rachio.webhook_url = hass_url + WEBHOOK_PATH + + # Get the API user + try: + person = RachioPerson(hass, rachio) + except AssertionError as error: + _LOGGER.error("Could not reach the Rachio API: %s", error) + return False + + # Check for Rachio controller devices + if not person.controllers: + _LOGGER.error("No Rachio devices found in account %s", + person.username) + return False + else: + _LOGGER.info("%d Rachio device(s) found", len(person.controllers)) + + # Enable component + hass.data[DOMAIN] = person + return True + + +class RachioPerson(object): + """Represent a Rachio user.""" + + def __init__(self, hass, rachio): + """Create an object from the provided API instance.""" + # Use API token to get user ID + self._hass = hass + self.rachio = rachio + + response = rachio.person.getInfo() + assert int(response[0][KEY_STATUS]) == 200, "API key error" + self._id = response[1][KEY_ID] + + # Use user ID to get user data + data = rachio.person.get(self._id) + assert int(data[0][KEY_STATUS]) == 200, "User ID error" + self.username = data[1][KEY_USERNAME] + self._controllers = [RachioIro(self._hass, self.rachio, controller) + for controller in data[1][KEY_DEVICES]] + _LOGGER.info('Using Rachio API as user "%s"', self.username) + + @property + def user_id(self) -> str: + """Get the user ID as defined by the Rachio API.""" + return self._id + + @property + def controllers(self) -> list: + """Get a list of controllers managed by this account.""" + return self._controllers + + +class RachioIro(object): + """Represent a Rachio Iro.""" + + def __init__(self, hass, rachio, data): + """Initialize a Rachio device.""" + self.hass = hass + self.rachio = rachio + self._id = data[KEY_ID] + self._name = data[KEY_NAME] + self._zones = data[KEY_ZONES] + self._init_data = data + _LOGGER.debug('%s has ID "%s"', str(self), self.controller_id) + + # Listen for all updates + self._init_webhooks() + + def _init_webhooks(self) -> None: + """Start getting updates from the Rachio API.""" + current_webhook_id = None + + # First delete any old webhooks that may have stuck around + def _deinit_webhooks(event) -> None: + """Stop getting updates from the Rachio API.""" + webhooks = self.rachio.notification.getDeviceWebhook( + self.controller_id)[1] + for webhook in webhooks: + if webhook[KEY_EXTERNAL_ID].startswith(WEBHOOK_CONST_ID) or\ + webhook[KEY_ID] == current_webhook_id: + self.rachio.notification.deleteWebhook(webhook[KEY_ID]) + _deinit_webhooks(None) + + # Choose which events to listen for and get their IDs + event_types = [] + for event_type in self.rachio.notification.getWebhookEventType()[1]: + if event_type[KEY_NAME] in LISTEN_EVENT_TYPES: + event_types.append({"id": event_type[KEY_ID]}) + + # Register to listen to these events from the device + url = self.rachio.webhook_url + auth = WEBHOOK_CONST_ID + self.rachio.webhook_auth + new_webhook = self.rachio.notification.postWebhook(self.controller_id, + auth, url, + event_types) + # Save ID for deletion at shutdown + current_webhook_id = new_webhook[1][KEY_ID] + self.hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _deinit_webhooks) + + def __str__(self) -> str: + """Display the controller as a string.""" + return 'Rachio controller "{}"'.format(self.name) + + @property + def controller_id(self) -> str: + """Return the Rachio API controller ID.""" + return self._id + + @property + def name(self) -> str: + """Return the user-defined name of the controller.""" + return self._name + + @property + def current_schedule(self) -> str: + """Return the schedule that the device is running right now.""" + return self.rachio.device.getCurrentSchedule(self.controller_id)[1] + + @property + def init_data(self) -> dict: + """Return the information used to set up the controller.""" + return self._init_data + + def list_zones(self, include_disabled=False) -> list: + """Return a list of the zone dicts connected to the device.""" + # All zones + if include_disabled: + return self._zones + + # Only enabled zones + return [z for z in self._zones if z[KEY_ENABLED]] + + def get_zone(self, zone_id) -> dict or None: + """Return the zone with the given ID.""" + for zone in self.list_zones(include_disabled=True): + if zone[KEY_ID] == zone_id: + return zone + + return None + + def stop_watering(self) -> None: + """Stop watering all zones connected to this controller.""" + self.rachio.device.stopWater(self.controller_id) + _LOGGER.info("Stopped watering of all zones on %s", str(self)) + + +class RachioWebhookView(HomeAssistantView): + """Provide a page for the server to call.""" + + SIGNALS = { + TYPE_CONTROLLER_STATUS: SIGNAL_RACHIO_CONTROLLER_UPDATE, + TYPE_SCHEDULE_STATUS: SIGNAL_RACHIO_SCHEDULE_UPDATE, + TYPE_ZONE_STATUS: SIGNAL_RACHIO_ZONE_UPDATE, + } + + requires_auth = False # Handled separately + url = WEBHOOK_PATH + name = url[1:].replace('/', ':') + + # pylint: disable=no-self-use + @asyncio.coroutine + async def post(self, request) -> web.Response: + """Handle webhook calls from the server.""" + hass = request.app['hass'] + data = await request.json() + + try: + auth = data.get(KEY_EXTERNAL_ID, str()).split(':')[1] + assert auth == hass.data[DOMAIN].rachio.webhook_auth + except (AssertionError, IndexError): + return web.Response(status=web.HTTPForbidden.status_code) + + update_type = data[KEY_TYPE] + if update_type in self.SIGNALS: + async_dispatcher_send(hass, self.SIGNALS[update_type], data) + + return web.Response(status=web.HTTPNoContent.status_code) diff --git a/homeassistant/components/switch/rachio.py b/homeassistant/components/switch/rachio.py index dc661c3e5bf..5f0ca995c90 100644 --- a/homeassistant/components/switch/rachio.py +++ b/homeassistant/components/switch/rachio.py @@ -4,227 +4,239 @@ Integration with the Rachio Iro sprinkler system controller. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.rachio/ """ +from abc import abstractmethod from datetime import timedelta import logging - import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.components.rachio import (DOMAIN as DOMAIN_RACHIO, + KEY_DEVICE_ID, + KEY_ENABLED, + KEY_ID, + KEY_NAME, + KEY_ON, + KEY_SUBTYPE, + KEY_SUMMARY, + KEY_ZONE_ID, + KEY_ZONE_NUMBER, + SIGNAL_RACHIO_CONTROLLER_UPDATE, + SIGNAL_RACHIO_ZONE_UPDATE, + SUBTYPE_ZONE_STARTED, + SUBTYPE_ZONE_STOPPED, + SUBTYPE_ZONE_COMPLETED, + SUBTYPE_SLEEP_MODE_ON, + SUBTYPE_SLEEP_MODE_OFF) import homeassistant.helpers.config_validation as cv -import homeassistant.util as util +from homeassistant.helpers.dispatcher import dispatcher_connect -REQUIREMENTS = ['rachiopy==0.1.2'] +DEPENDENCIES = ['rachio'] _LOGGER = logging.getLogger(__name__) +# Manual run length CONF_MANUAL_RUN_MINS = 'manual_run_mins' - -DATA_RACHIO = 'rachio' - DEFAULT_MANUAL_RUN_MINS = 10 -MIN_UPDATE_INTERVAL = timedelta(seconds=30) -MIN_FORCED_UPDATE_INTERVAL = timedelta(seconds=1) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ACCESS_TOKEN): cv.string, vol.Optional(CONF_MANUAL_RUN_MINS, default=DEFAULT_MANUAL_RUN_MINS): cv.positive_int, }) +ATTR_ZONE_SUMMARY = 'Summary' +ATTR_ZONE_NUMBER = 'Zone number' + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Rachio switches.""" - from rachiopy import Rachio + manual_run_time = timedelta(minutes=config.get(CONF_MANUAL_RUN_MINS)) + _LOGGER.info("Rachio run time is %s", str(manual_run_time)) - # Get options - manual_run_mins = config.get(CONF_MANUAL_RUN_MINS) - _LOGGER.debug("Rachio run time is %d min", manual_run_mins) + # Add all zones from all controllers as switches + devices = [] + for controller in hass.data[DOMAIN_RACHIO].controllers: + devices.append(RachioStandbySwitch(hass, controller)) - access_token = config.get(CONF_ACCESS_TOKEN) + for zone in controller.list_zones(): + devices.append(RachioZone(hass, controller, zone, manual_run_time)) - # Configure API - _LOGGER.debug("Configuring Rachio API") - rachio = Rachio(access_token) - - person = None - try: - person = _get_person(rachio) - except KeyError: - _LOGGER.error( - "Could not reach the Rachio API. Is your access token valid?") - return - - # Get and persist devices - devices = _list_devices(rachio, manual_run_mins) - if not devices: - _LOGGER.error( - "No Rachio devices found in account %s", person['username']) - return - - hass.data[DATA_RACHIO] = devices[0] - - if len(devices) > 1: - _LOGGER.warning("Multiple Rachio devices found in account, " - "using %s", hass.data[DATA_RACHIO].device_id) - else: - _LOGGER.debug("Found Rachio device") - - hass.data[DATA_RACHIO].update() - add_devices(hass.data[DATA_RACHIO].list_zones()) + add_devices(devices) + _LOGGER.info("%d Rachio switch(es) added", len(devices)) -def _get_person(rachio): - """Pull the account info of the person whose access token was provided.""" - person_id = rachio.person.getInfo()[1]['id'] - return rachio.person.get(person_id)[1] +class RachioSwitch(SwitchDevice): + """Represent a Rachio state that can be toggled.""" + def __init__(self, controller, poll=True): + """Initialize a new Rachio switch.""" + self._controller = controller -def _list_devices(rachio, manual_run_mins): - """Pull a list of devices on the account.""" - return [RachioIro(rachio, d['id'], manual_run_mins) - for d in _get_person(rachio)['devices']] - - -class RachioIro(object): - """Representation of a Rachio Iro.""" - - def __init__(self, rachio, device_id, manual_run_mins): - """Initialize a Rachio device.""" - self.rachio = rachio - self._device_id = device_id - self.manual_run_mins = manual_run_mins - self._device = None - self._running = None - self._zones = None - - def __str__(self): - """Display the device as a string.""" - return "Rachio Iro {}".format(self.serial_number) + if poll: + self._state = self._poll_update() + else: + self._state = None @property - def device_id(self): - """Return the Rachio API device ID.""" - return self._device['id'] + def should_poll(self) -> bool: + """Declare that this entity pushes its state to HA.""" + return False @property - def status(self): - """Return the current status of the device.""" - return self._device['status'] + def name(self) -> str: + """Get a name for this switch.""" + return "Switch on {}".format(self._controller.name) @property - def serial_number(self): - """Return the serial number of the device.""" - return self._device['serialNumber'] + def is_on(self) -> bool: + """Return whether the switch is currently on.""" + return self._state + + @abstractmethod + def _poll_update(self, data=None) -> bool: + """Poll the API.""" + pass + + def _handle_any_update(self, *args, **kwargs) -> None: + """Determine whether an update event applies to this device.""" + if args[0][KEY_DEVICE_ID] != self._controller.controller_id: + # For another device + return + + # For this device + self._handle_update(args, kwargs) + + @abstractmethod + def _handle_update(self, *args, **kwargs) -> None: + """Handle incoming webhook data.""" + pass + + +class RachioStandbySwitch(RachioSwitch): + """Representation of a standby status/button.""" + + def __init__(self, hass, controller): + """Instantiate a new Rachio standby mode switch.""" + dispatcher_connect(hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, + self._handle_any_update) + super().__init__(controller, poll=False) + self._poll_update(controller.init_data) @property - def is_paused(self): - """Return whether the device is temporarily disabled.""" - return self._device['paused'] + def name(self) -> str: + """Return the name of the standby switch.""" + return "{} in standby mode".format(self._controller.name) @property - def is_on(self): - """Return whether the device is powered on and connected.""" - return self._device['on'] + def icon(self) -> str: + """Return an icon for the standby switch.""" + return "mdi:power" - @property - def current_schedule(self): - """Return the schedule that the device is running right now.""" - return self._running + def _poll_update(self, data=None) -> bool: + """Request the state from the API.""" + if data is None: + data = self._controller.rachio.device.get( + self._controller.controller_id)[1] - def list_zones(self, include_disabled=False): - """Return a list of the zones connected to the device, incl. data.""" - if not self._zones: - self._zones = [RachioZone(self.rachio, self, zone['id'], - self.manual_run_mins) - for zone in self._device['zones']] + return not data[KEY_ON] - if include_disabled: - return self._zones + def _handle_update(self, *args, **kwargs) -> None: + """Update the state using webhook data.""" + if args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON: + self._state = True + elif args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF: + self._state = False - self.update(no_throttle=True) - return [z for z in self._zones if z.is_enabled] + self.schedule_update_ha_state() - @util.Throttle(MIN_UPDATE_INTERVAL, MIN_FORCED_UPDATE_INTERVAL) - def update(self, **kwargs): - """Pull updated device info from the Rachio API.""" - self._device = self.rachio.device.get(self._device_id)[1] - self._running = self.rachio.device\ - .getCurrentSchedule(self._device_id)[1] + def turn_on(self, **kwargs) -> None: + """Put the controller in standby mode.""" + self._controller.rachio.device.off(self._controller.controller_id) - # Possibly update all zones - for zone in self.list_zones(include_disabled=True): - zone.update() - - _LOGGER.debug("Updated %s", str(self)) + def turn_off(self, **kwargs) -> None: + """Resume controller functionality.""" + self._controller.rachio.device.on(self._controller.controller_id) -class RachioZone(SwitchDevice): +class RachioZone(RachioSwitch): """Representation of one zone of sprinklers connected to the Rachio Iro.""" - def __init__(self, rachio, device, zone_id, manual_run_mins): + def __init__(self, hass, controller, data, manual_run_time): """Initialize a new Rachio Zone.""" - self.rachio = rachio - self._device = device - self._zone_id = zone_id - self._zone = None - self._manual_run_secs = manual_run_mins * 60 + self._id = data[KEY_ID] + self._zone_name = data[KEY_NAME] + self._zone_number = data[KEY_ZONE_NUMBER] + self._zone_enabled = data[KEY_ENABLED] + self._manual_run_time = manual_run_time + self._summary = str() + super().__init__(controller) + + # Listen for all zone updates + dispatcher_connect(hass, SIGNAL_RACHIO_ZONE_UPDATE, + self._handle_update) def __str__(self): """Display the zone as a string.""" - return "Rachio Zone {}".format(self.name) + return 'Rachio Zone "{}" on {}'.format(self.name, + str(self._controller)) @property - def zone_id(self): + def zone_id(self) -> str: """How the Rachio API refers to the zone.""" - return self._zone['id'] + return self._id @property - def unique_id(self): - """Return the unique string ID for the zone.""" - return '{iro}-{zone}'.format( - iro=self._device.device_id, zone=self.zone_id) - - @property - def number(self): - """Return the physical connection of the zone pump.""" - return self._zone['zoneNumber'] - - @property - def name(self): + def name(self) -> str: """Return the friendly name of the zone.""" - return self._zone['name'] + return self._zone_name @property - def is_enabled(self): + def icon(self) -> str: + """Return the icon to display.""" + return "mdi:water" + + @property + def zone_is_enabled(self) -> bool: """Return whether the zone is allowed to run.""" - return self._zone['enabled'] + return self._zone_enabled @property - def is_on(self): - """Return whether the zone is currently running.""" - schedule = self._device.current_schedule - return self.zone_id == schedule.get('zoneId') + def state_attributes(self) -> dict: + """Return the optional state attributes.""" + return { + ATTR_ZONE_NUMBER: self._zone_number, + ATTR_ZONE_SUMMARY: self._summary, + } - def update(self): - """Pull updated zone info from the Rachio API.""" - self._zone = self.rachio.zone.get(self._zone_id)[1] - - # Possibly update device - self._device.update() - - _LOGGER.debug("Updated %s", str(self)) - - def turn_on(self, **kwargs): - """Start the zone.""" + def turn_on(self, **kwargs) -> None: + """Start watering this zone.""" # Stop other zones first self.turn_off() - _LOGGER.info("Watering %s for %d s", self.name, self._manual_run_secs) - self.rachio.zone.start(self.zone_id, self._manual_run_secs) + # Start this zone + self._controller.rachio.zone.start(self.zone_id, + self._manual_run_time.seconds) + _LOGGER.debug("Watering %s on %s", self.name, self._controller.name) - def turn_off(self, **kwargs): - """Stop all zones.""" - _LOGGER.info("Stopping watering of all zones") - self.rachio.device.stopWater(self._device.device_id) + def turn_off(self, **kwargs) -> None: + """Stop watering all zones.""" + self._controller.stop_watering() + + def _poll_update(self, data=None) -> bool: + """Poll the API to check whether the zone is running.""" + schedule = self._controller.current_schedule + return self.zone_id == schedule.get(KEY_ZONE_ID) + + def _handle_update(self, *args, **kwargs) -> None: + """Handle incoming webhook zone data.""" + if args[0][KEY_ZONE_ID] != self.zone_id: + return + + self._summary = kwargs.get(KEY_SUMMARY, str()) + + if args[0][KEY_SUBTYPE] == SUBTYPE_ZONE_STARTED: + self._state = True + elif args[0][KEY_SUBTYPE] in [SUBTYPE_ZONE_STOPPED, + SUBTYPE_ZONE_COMPLETED]: + self._state = False + + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index a7422b971b0..b011bd6747e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1163,8 +1163,8 @@ pyzabbix==0.7.4 # homeassistant.components.sensor.qnap qnapstats==0.2.6 -# homeassistant.components.switch.rachio -rachiopy==0.1.2 +# homeassistant.components.rachio +rachiopy==0.1.3 # homeassistant.components.climate.radiotherm radiotherm==1.3 From 11ba7cc8ce7fe9212ff9258b56bd4b392b703f3d Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sun, 1 Jul 2018 10:36:50 -0700 Subject: [PATCH 111/128] Only create front-end client_id once (#15214) * Only create frontend client_id once * Check user and client_id before create refresh token * Lint * Follow code review comment * Minor clenaup * Update doc string --- homeassistant/auth.py | 105 ++++++++++++------ homeassistant/components/frontend/__init__.py | 2 +- tests/common.py | 10 +- tests/components/auth/__init__.py | 2 +- tests/test_auth.py | 53 +++++++-- 5 files changed, 121 insertions(+), 51 deletions(-) diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 22abcdf213c..767776f7ad9 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -1,23 +1,22 @@ """Provide an authentication layer for Home Assistant.""" import asyncio import binascii -from collections import OrderedDict -from datetime import datetime, timedelta -import os import importlib import logging +import os import uuid +from collections import OrderedDict +from datetime import datetime, timedelta import attr import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements -from homeassistant.core import callback from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID -from homeassistant.util.decorator import Registry +from homeassistant.core import callback from homeassistant.util import dt as dt_util - +from homeassistant.util.decorator import Registry _LOGGER = logging.getLogger(__name__) @@ -337,6 +336,16 @@ class AuthManager: return await self._store.async_create_client( name, redirect_uris, no_secret) + async def async_get_or_create_client(self, name, *, redirect_uris=None, + no_secret=False): + """Find a client, if not exists, create a new one.""" + for client in await self._store.async_get_clients(): + if client.name == name: + return client + + return await self._store.async_create_client( + name, redirect_uris, no_secret) + async def async_get_client(self, client_id): """Get a client.""" return await self._store.async_get_client(client_id) @@ -380,29 +389,36 @@ class AuthStore: def __init__(self, hass): """Initialize the auth store.""" self.hass = hass - self.users = None - self.clients = None + self._users = None + self._clients = None self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) async def credentials_for_provider(self, provider_type, provider_id): """Return credentials for specific auth provider type and id.""" - if self.users is None: + if self._users is None: await self.async_load() return [ credentials - for user in self.users.values() + for user in self._users.values() for credentials in user.credentials if (credentials.auth_provider_type == provider_type and credentials.auth_provider_id == provider_id) ] - async def async_get_user(self, user_id): - """Retrieve a user.""" - if self.users is None: + async def async_get_users(self): + """Retrieve all users.""" + if self._users is None: await self.async_load() - return self.users.get(user_id) + return list(self._users.values()) + + async def async_get_user(self, user_id): + """Retrieve a user.""" + if self._users is None: + await self.async_load() + + return self._users.get(user_id) async def async_get_or_create_user(self, credentials, auth_provider): """Get or create a new user for given credentials. @@ -410,7 +426,7 @@ class AuthStore: If link_user is passed in, the credentials will be linked to the passed in user if the credentials are new. """ - if self.users is None: + if self._users is None: await self.async_load() # New credentials, store in user @@ -418,7 +434,7 @@ class AuthStore: info = await auth_provider.async_user_meta_for_credentials( credentials) # Make owner and activate user if it's the first user. - if self.users: + if self._users: is_owner = False is_active = False else: @@ -430,11 +446,11 @@ class AuthStore: is_active=is_active, name=info.get('name'), ) - self.users[new_user.id] = new_user + self._users[new_user.id] = new_user await self.async_link_user(new_user, credentials) return new_user - for user in self.users.values(): + for user in self._users.values(): for creds in user.credentials: if (creds.auth_provider_type == credentials.auth_provider_type and creds.auth_provider_id == @@ -451,11 +467,19 @@ class AuthStore: async def async_remove_user(self, user): """Remove a user.""" - self.users.pop(user.id) + self._users.pop(user.id) await self.async_save() async def async_create_refresh_token(self, user, client_id): """Create a new token for a user.""" + local_user = await self.async_get_user(user.id) + if local_user is None: + raise ValueError('Invalid user') + + local_client = await self.async_get_client(client_id) + if local_client is None: + raise ValueError('Invalid client_id') + refresh_token = RefreshToken(user, client_id) user.refresh_tokens[refresh_token.token] = refresh_token await self.async_save() @@ -463,10 +487,10 @@ class AuthStore: async def async_get_refresh_token(self, token): """Get refresh token by token.""" - if self.users is None: + if self._users is None: await self.async_load() - for user in self.users.values(): + for user in self._users.values(): refresh_token = user.refresh_tokens.get(token) if refresh_token is not None: return refresh_token @@ -475,7 +499,7 @@ class AuthStore: async def async_create_client(self, name, redirect_uris, no_secret): """Create a new client.""" - if self.clients is None: + if self._clients is None: await self.async_load() kwargs = { @@ -487,16 +511,23 @@ class AuthStore: kwargs['secret'] = None client = Client(**kwargs) - self.clients[client.id] = client + self._clients[client.id] = client await self.async_save() return client - async def async_get_client(self, client_id): - """Get a client.""" - if self.clients is None: + async def async_get_clients(self): + """Return all clients.""" + if self._clients is None: await self.async_load() - return self.clients.get(client_id) + return list(self._clients.values()) + + async def async_get_client(self, client_id): + """Get a client.""" + if self._clients is None: + await self.async_load() + + return self._clients.get(client_id) async def async_load(self): """Load the users.""" @@ -504,12 +535,12 @@ class AuthStore: # Make sure that we're not overriding data if 2 loads happened at the # same time - if self.users is not None: + if self._users is not None: return if data is None: - self.users = {} - self.clients = {} + self._users = {} + self._clients = {} return users = { @@ -553,8 +584,8 @@ class AuthStore: cl_dict['id']: Client(**cl_dict) for cl_dict in data['clients'] } - self.users = users - self.clients = clients + self._users = users + self._clients = clients async def async_save(self): """Save users.""" @@ -565,7 +596,7 @@ class AuthStore: 'is_active': user.is_active, 'name': user.name, } - for user in self.users.values() + for user in self._users.values() ] credentials = [ @@ -576,7 +607,7 @@ class AuthStore: 'auth_provider_id': credential.auth_provider_id, 'data': credential.data, } - for user in self.users.values() + for user in self._users.values() for credential in user.credentials ] @@ -590,7 +621,7 @@ class AuthStore: refresh_token.access_token_expiration.total_seconds(), 'token': refresh_token.token, } - for user in self.users.values() + for user in self._users.values() for refresh_token in user.refresh_tokens.values() ] @@ -601,7 +632,7 @@ class AuthStore: 'created_at': access_token.created_at.isoformat(), 'token': access_token.token, } - for user in self.users.values() + for user in self._users.values() for refresh_token in user.refresh_tokens.values() for access_token in refresh_token.access_tokens ] @@ -613,7 +644,7 @@ class AuthStore: 'secret': client.secret, 'redirect_uris': client.redirect_uris, } - for client in self.clients.values() + for client in self._clients.values() ] data = { diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7bad8ff727d..9a32626c66a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -201,7 +201,7 @@ def add_manifest_json_key(key, val): async def async_setup(hass, config): """Set up the serving of the frontend.""" if hass.auth.active: - client = await hass.auth.async_create_client( + client = await hass.auth.async_get_or_create_client( 'Home Assistant Frontend', redirect_uris=['/'], no_secret=True, diff --git a/tests/common.py b/tests/common.py index 1b8eabaa0db..3a51cd3e059 100644 --- a/tests/common.py +++ b/tests/common.py @@ -321,7 +321,7 @@ class MockUser(auth.User): def add_to_auth_manager(self, auth_mgr): """Test helper to add entry to hass.""" ensure_auth_manager_loaded(auth_mgr) - auth_mgr._store.users[self.id] = self + auth_mgr._store._users[self.id] = self return self @@ -329,10 +329,10 @@ class MockUser(auth.User): def ensure_auth_manager_loaded(auth_mgr): """Ensure an auth manager is considered loaded.""" store = auth_mgr._store - if store.clients is None: - store.clients = {} - if store.users is None: - store.users = {} + if store._clients is None: + store._clients = {} + if store._users is None: + store._users = {} class MockModule(object): diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py index f0b205ff5ce..21719c12569 100644 --- a/tests/components/auth/__init__.py +++ b/tests/components/auth/__init__.py @@ -34,7 +34,7 @@ async def async_setup_auth(hass, aiohttp_client, provider_configs=BASE_CONFIG, }) client = auth.Client('Test Client', CLIENT_ID, CLIENT_SECRET, redirect_uris=[CLIENT_REDIRECT_URI]) - hass.auth._store.clients[client.id] = client + hass.auth._store._clients[client.id] = client if setup_api: await async_setup_component(hass, 'api', {}) return await aiohttp_client(hass.http.app) diff --git a/tests/test_auth.py b/tests/test_auth.py index 4c0db71466e..5b545223c15 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -191,12 +191,13 @@ async def test_saving_loading(hass, hass_storage): await flush_store(manager._store._store) store2 = auth.AuthStore(hass) - await store2.async_load() - assert len(store2.users) == 1 - assert store2.users[user.id] == user + users = await store2.async_get_users() + assert len(users) == 1 + assert users[0] == user - assert len(store2.clients) == 1 - assert store2.clients[client.id] == client + clients = await store2.async_get_clients() + assert len(clients) == 1 + assert clients[0] == client def test_access_token_expired(): @@ -224,15 +225,18 @@ def test_access_token_expired(): async def test_cannot_retrieve_expired_access_token(hass): """Test that we cannot retrieve expired access tokens.""" manager = await auth.auth_manager_from_config(hass, []) + client = await manager.async_create_client('test') user = MockUser( id='mock-user', is_owner=False, is_active=False, name='Paulus', ).add_to_auth_manager(manager) - refresh_token = await manager.async_create_refresh_token(user, 'bla') - access_token = manager.async_create_access_token(refresh_token) + refresh_token = await manager.async_create_refresh_token(user, client.id) + assert refresh_token.user.id is user.id + assert refresh_token.client_id is client.id + access_token = manager.async_create_access_token(refresh_token) assert manager.async_get_access_token(access_token.token) is access_token with patch('homeassistant.auth.dt_util.utcnow', @@ -241,3 +245,38 @@ async def test_cannot_retrieve_expired_access_token(hass): # Even with unpatched time, it should have been removed from manager assert manager.async_get_access_token(access_token.token) is None + + +async def test_get_or_create_client(hass): + """Test that get_or_create_client works.""" + manager = await auth.auth_manager_from_config(hass, []) + + client1 = await manager.async_get_or_create_client( + 'Test Client', redirect_uris=['https://test.com/1']) + assert client1.name is 'Test Client' + + client2 = await manager.async_get_or_create_client( + 'Test Client', redirect_uris=['https://test.com/1']) + assert client2.id is client1.id + + +async def test_cannot_create_refresh_token_with_invalide_client_id(hass): + """Test that we cannot create refresh token with invalid client id.""" + manager = await auth.auth_manager_from_config(hass, []) + user = MockUser( + id='mock-user', + is_owner=False, + is_active=False, + name='Paulus', + ).add_to_auth_manager(manager) + with pytest.raises(ValueError): + await manager.async_create_refresh_token(user, 'bla') + + +async def test_cannot_create_refresh_token_with_invalide_user(hass): + """Test that we cannot create refresh token with invalid client id.""" + manager = await auth.auth_manager_from_config(hass, []) + client = await manager.async_create_client('test') + user = MockUser(id='invalid-user') + with pytest.raises(ValueError): + await manager.async_create_refresh_token(user, client.id) From 47401739eaf97f97b1e2c348c878b04fd92e4d39 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 1 Jul 2018 19:06:30 +0200 Subject: [PATCH 112/128] Make LIFX color/temperature attributes mutually exclusive (#15234) --- homeassistant/components/light/lifx.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 421356f07bc..9b2c183c1d1 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -446,7 +446,9 @@ class LIFXLight(Light): @property def color_temp(self): """Return the color temperature.""" - kelvin = self.device.color[3] + _, sat, _, kelvin = self.device.color + if sat: + return None return color_util.color_temperature_kelvin_to_mired(kelvin) @property @@ -601,7 +603,7 @@ class LIFXColor(LIFXLight): hue, sat, _, _ = self.device.color hue = hue / 65535 * 360 sat = sat / 65535 * 100 - return (hue, sat) + return (hue, sat) if sat else None class LIFXStrip(LIFXColor): From 311a44007ca21b2181fd46e083da377ceffc3a2e Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sun, 1 Jul 2018 10:04:12 -0700 Subject: [PATCH 113/128] Fix an issue when user's nest developer account don't have permission (#15237) --- homeassistant/components/binary_sensor/nest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 9da352e1268..31460c1eedc 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -31,12 +31,10 @@ CAMERA_BINARY_TYPES = { STRUCTURE_BINARY_TYPES = { 'away': None, - # 'security_state', # pending python-nest update } STRUCTURE_BINARY_STATE_MAP = { 'away': {'away': True, 'home': False}, - 'security_state': {'deter': True, 'ok': False}, } _BINARY_TYPES_DEPRECATED = [ @@ -135,7 +133,7 @@ class NestBinarySensor(NestSensorDevice, BinarySensorDevice): value = getattr(self.device, self.variable) if self.variable in STRUCTURE_BINARY_TYPES: self._state = bool(STRUCTURE_BINARY_STATE_MAP - [self.variable][value]) + [self.variable].get(value)) else: self._state = bool(value) From c978281d1eb0860c8f5d6f285772967d91a83dde Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 1 Jul 2018 17:48:54 +0200 Subject: [PATCH 114/128] Revert some changes to setup.py (#15248) --- setup.cfg | 14 -------------- setup.py | 12 +++++++++++- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/setup.cfg b/setup.cfg index 2abd445bb85..7813cc5c047 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,20 +15,6 @@ classifier = Programming Language :: Python :: 3.6 Topic :: Home Automation -[options] -packages = find: -include_package_data = true -zip_safe = false - -[options.entry_points] -console_scripts = - hass = homeassistant.__main__:main - -[options.packages.find] -exclude = - tests - tests.* - [tool:pytest] testpaths = tests norecursedirs = .git testing_config diff --git a/setup.py b/setup.py index 3833f90f2d1..928d894c9d1 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Home Assistant setup script.""" from datetime import datetime as dt -from setuptools import setup +from setuptools import setup, find_packages import homeassistant.const as hass_const @@ -29,6 +29,8 @@ PROJECT_URLS = { 'Forum': 'https://community.home-assistant.io/', } +PACKAGES = find_packages(exclude=['tests', 'tests.*']) + REQUIRES = [ 'aiohttp==3.3.2', 'astral==1.6.1', @@ -53,7 +55,15 @@ setup( project_urls=PROJECT_URLS, author=PROJECT_AUTHOR, author_email=PROJECT_EMAIL, + packages=PACKAGES, + include_package_data=True, + zip_safe=False, install_requires=REQUIRES, python_requires='>={}'.format(MIN_PY_VERSION), test_suite='tests', + entry_points={ + 'console_scripts': [ + 'hass = homeassistant.__main__:main' + ] + }, ) From 279fd39677f4d06d54afe2c03cc7b9d699baf1c4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Jul 2018 13:40:55 -0400 Subject: [PATCH 115/128] Bumped version to 0.73.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8511941ce02..6fd41b5f4d2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 73 -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 63b28aa39d7544b5e115a07e0f75633d0f3d07b0 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sat, 30 Jun 2018 19:31:36 -0700 Subject: [PATCH 116/128] By default to use access_token if hass.auth.active (#15212) * Force to use access_token if hass.auth.active * Not allow Basic auth with api_password if hass.auth.active * Block websocket api_password auth when hass.auth.active * Add legacy_api_password auth provider * lint * lint --- homeassistant/auth.py | 14 +- .../auth_providers/legacy_api_password.py | 104 ++++++++++++ homeassistant/components/http/__init__.py | 17 +- homeassistant/components/http/auth.py | 66 ++++---- homeassistant/components/websocket_api.py | 24 +-- .../test_legacy_api_password.py | 67 ++++++++ tests/components/http/test_auth.py | 151 +++++++++++++++--- tests/components/test_websocket_api.py | 108 ++++++++++--- 8 files changed, 468 insertions(+), 83 deletions(-) create mode 100644 homeassistant/auth_providers/legacy_api_password.py create mode 100644 tests/auth_providers/test_legacy_api_password.py diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 767776f7ad9..a4e8ee05943 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -279,6 +279,18 @@ class AuthManager: """Return if any auth providers are registered.""" return bool(self._providers) + @property + def support_legacy(self): + """ + Return if legacy_api_password auth providers are registered. + + Should be removed when we removed legacy_api_password auth providers. + """ + for provider_type, _ in self._providers: + if provider_type == 'legacy_api_password': + return True + return False + @property def async_auth_providers(self): """Return a list of available auth providers.""" @@ -565,7 +577,7 @@ class AuthStore: client_id=rt_dict['client_id'], created_at=dt_util.parse_datetime(rt_dict['created_at']), access_token_expiration=timedelta( - rt_dict['access_token_expiration']), + seconds=rt_dict['access_token_expiration']), token=rt_dict['token'], ) refresh_tokens[token.id] = token diff --git a/homeassistant/auth_providers/legacy_api_password.py b/homeassistant/auth_providers/legacy_api_password.py new file mode 100644 index 00000000000..510cc4d0279 --- /dev/null +++ b/homeassistant/auth_providers/legacy_api_password.py @@ -0,0 +1,104 @@ +""" +Support Legacy API password auth provider. + +It will be removed when auth system production ready +""" +from collections import OrderedDict +import hmac + +import voluptuous as vol + +from homeassistant.exceptions import HomeAssistantError +from homeassistant import auth, data_entry_flow +from homeassistant.core import callback + +USER_SCHEMA = vol.Schema({ + vol.Required('username'): str, +}) + + +CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ +}, extra=vol.PREVENT_EXTRA) + +LEGACY_USER = 'homeassistant' + + +class InvalidAuthError(HomeAssistantError): + """Raised when submitting invalid authentication.""" + + +@auth.AUTH_PROVIDERS.register('legacy_api_password') +class LegacyApiPasswordAuthProvider(auth.AuthProvider): + """Example auth provider based on hardcoded usernames and passwords.""" + + DEFAULT_TITLE = 'Legacy API Password' + + async def async_credential_flow(self): + """Return a flow to login.""" + return LoginFlow(self) + + @callback + def async_validate_login(self, password): + """Helper to validate a username and password.""" + if not hasattr(self.hass, 'http'): + raise ValueError('http component is not loaded') + + if self.hass.http.api_password is None: + raise ValueError('http component is not configured using' + ' api_password') + + if not hmac.compare_digest(self.hass.http.api_password.encode('utf-8'), + password.encode('utf-8')): + raise InvalidAuthError + + async def async_get_or_create_credentials(self, flow_result): + """Return LEGACY_USER always.""" + for credential in await self.async_credentials(): + if credential.data['username'] == LEGACY_USER: + return credential + + return self.async_create_credentials({ + 'username': LEGACY_USER + }) + + async def async_user_meta_for_credentials(self, credentials): + """ + Set name as LEGACY_USER always. + + Will be used to populate info when creating a new user. + """ + return {'name': LEGACY_USER} + + +class LoginFlow(data_entry_flow.FlowHandler): + """Handler for the login flow.""" + + def __init__(self, auth_provider): + """Initialize the login flow.""" + self._auth_provider = auth_provider + + async def async_step_init(self, user_input=None): + """Handle the step of the form.""" + errors = {} + + if user_input is not None: + try: + self._auth_provider.async_validate_login( + user_input['password']) + except InvalidAuthError: + errors['base'] = 'invalid_auth' + + if not errors: + return self.async_create_entry( + title=self._auth_provider.name, + data={} + ) + + schema = OrderedDict() + schema['password'] = str + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema(schema), + errors=errors, + ) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 485433434fd..37a6805dfb5 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -184,7 +184,22 @@ class HomeAssistantHTTP(object): if is_ban_enabled: setup_bans(hass, app, login_threshold) - setup_auth(app, trusted_networks, api_password) + if hass.auth.active: + if hass.auth.support_legacy: + _LOGGER.warning("Experimental auth api enabled and " + "legacy_api_password support enabled. Please " + "use access_token instead api_password, " + "although you can still use legacy " + "api_password") + else: + _LOGGER.warning("Experimental auth api enabled. Please use " + "access_token instead api_password.") + elif api_password is None: + _LOGGER.warning("You have been advised to set http.api_password.") + + setup_auth(app, trusted_networks, hass.auth.active, + support_legacy=hass.auth.support_legacy, + api_password=api_password) if cors_origins: setup_cors(app, cors_origins) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index c4723abccee..a232d9295a4 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -17,37 +17,44 @@ _LOGGER = logging.getLogger(__name__) @callback -def setup_auth(app, trusted_networks, api_password): +def setup_auth(app, trusted_networks, use_auth, + support_legacy=False, api_password=None): """Create auth middleware for the app.""" @middleware async def auth_middleware(request, handler): """Authenticate as middleware.""" - # If no password set, just always set authenticated=True - if api_password is None: - request[KEY_AUTHENTICATED] = True - return await handler(request) - - # Check authentication authenticated = False - if (HTTP_HEADER_HA_AUTH in request.headers and - hmac.compare_digest( - api_password.encode('utf-8'), - request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))): + if use_auth and (HTTP_HEADER_HA_AUTH in request.headers or + DATA_API_PASSWORD in request.query): + _LOGGER.warning('Please use access_token instead api_password.') + + legacy_auth = (not use_auth or support_legacy) and api_password + if (hdrs.AUTHORIZATION in request.headers and + await async_validate_auth_header( + request, api_password if legacy_auth else None)): + # it included both use_auth and api_password Basic auth + authenticated = True + + elif (legacy_auth and HTTP_HEADER_HA_AUTH in request.headers and + hmac.compare_digest( + api_password.encode('utf-8'), + request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))): # A valid auth header has been set authenticated = True - elif (DATA_API_PASSWORD in request.query and + elif (legacy_auth and DATA_API_PASSWORD in request.query and hmac.compare_digest( api_password.encode('utf-8'), request.query[DATA_API_PASSWORD].encode('utf-8'))): authenticated = True - elif (hdrs.AUTHORIZATION in request.headers and - await async_validate_auth_header(api_password, request)): + elif _is_trusted_ip(request, trusted_networks): authenticated = True - elif _is_trusted_ip(request, trusted_networks): + elif not use_auth and api_password is None: + # If neither password nor auth_providers set, + # just always set authenticated=True authenticated = True request[KEY_AUTHENTICATED] = authenticated @@ -76,8 +83,12 @@ def validate_password(request, api_password): request.app['hass'].http.api_password.encode('utf-8')) -async def async_validate_auth_header(api_password, request): - """Test an authorization header if valid password.""" +async def async_validate_auth_header(request, api_password=None): + """ + Test authorization header against access token. + + Basic auth_type is legacy code, should be removed with api_password. + """ if hdrs.AUTHORIZATION not in request.headers: return False @@ -88,7 +99,16 @@ async def async_validate_auth_header(api_password, request): # If no space in authorization header return False - if auth_type == 'Basic': + if auth_type == 'Bearer': + hass = request.app['hass'] + access_token = hass.auth.async_get_access_token(auth_val) + if access_token is None: + return False + + request['hass_user'] = access_token.refresh_token.user + return True + + elif auth_type == 'Basic' and api_password is not None: decoded = base64.b64decode(auth_val).decode('utf-8') try: username, password = decoded.split(':', 1) @@ -102,13 +122,5 @@ async def async_validate_auth_header(api_password, request): return hmac.compare_digest(api_password.encode('utf-8'), password.encode('utf-8')) - if auth_type != 'Bearer': + else: return False - - hass = request.app['hass'] - access_token = hass.auth.async_get_access_token(auth_val) - if access_token is None: - return False - - request['hass_user'] = access_token.refresh_token.user - return True diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index bf472348bab..c26f68a2c29 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -315,26 +315,32 @@ class ActiveConnection: authenticated = True else: + self.debug("Request auth") await self.wsock.send_json(auth_required_message()) msg = await wsock.receive_json() msg = AUTH_MESSAGE_SCHEMA(msg) - if 'api_password' in msg: - authenticated = validate_password( - request, msg['api_password']) - - elif 'access_token' in msg: + if self.hass.auth.active and 'access_token' in msg: + self.debug("Received access_token") token = self.hass.auth.async_get_access_token( msg['access_token']) authenticated = token is not None + elif ((not self.hass.auth.active or + self.hass.auth.support_legacy) and + 'api_password' in msg): + self.debug("Received api_password") + authenticated = validate_password( + request, msg['api_password']) + if not authenticated: - self.debug("Invalid password") + self.debug("Authorization failed") await self.wsock.send_json( - auth_invalid_message('Invalid password')) + auth_invalid_message('Invalid access token or password')) await process_wrong_login(request) return wsock + self.debug("Auth OK") await self.wsock.send_json(auth_ok_message()) # ---------- AUTH PHASE OVER ---------- @@ -392,7 +398,7 @@ class ActiveConnection: if wsock.closed: self.debug("Connection closed by client") else: - _LOGGER.exception("Unexpected TypeError: %s", msg) + _LOGGER.exception("Unexpected TypeError: %s", err) except ValueError as err: msg = "Received invalid JSON" @@ -403,7 +409,7 @@ class ActiveConnection: self._writer_task.cancel() except CANCELLATION_ERRORS: - self.debug("Connection cancelled by server") + self.debug("Connection cancelled") except asyncio.QueueFull: self.log_error("Client exceeded max pending messages [1]:", diff --git a/tests/auth_providers/test_legacy_api_password.py b/tests/auth_providers/test_legacy_api_password.py new file mode 100644 index 00000000000..7a8f17894aa --- /dev/null +++ b/tests/auth_providers/test_legacy_api_password.py @@ -0,0 +1,67 @@ +"""Tests for the legacy_api_password auth provider.""" +from unittest.mock import Mock + +import pytest + +from homeassistant import auth +from homeassistant.auth_providers import legacy_api_password + + +@pytest.fixture +def store(hass): + """Mock store.""" + return auth.AuthStore(hass) + + +@pytest.fixture +def provider(hass, store): + """Mock provider.""" + return legacy_api_password.LegacyApiPasswordAuthProvider(hass, store, { + 'type': 'legacy_api_password', + }) + + +async def test_create_new_credential(provider): + """Test that we create a new credential.""" + credentials = await provider.async_get_or_create_credentials({}) + assert credentials.data["username"] is legacy_api_password.LEGACY_USER + assert credentials.is_new is True + + +async def test_only_one_credentials(store, provider): + """Call create twice will return same credential.""" + credentials = await provider.async_get_or_create_credentials({}) + await store.async_get_or_create_user(credentials, provider) + credentials2 = await provider.async_get_or_create_credentials({}) + assert credentials2.data["username"] is legacy_api_password.LEGACY_USER + assert credentials2.id is credentials.id + assert credentials2.is_new is False + + +async def test_verify_not_load(hass, provider): + """Test we raise if http module not load.""" + with pytest.raises(ValueError): + provider.async_validate_login('test-password') + hass.http = Mock(api_password=None) + with pytest.raises(ValueError): + provider.async_validate_login('test-password') + hass.http = Mock(api_password='test-password') + provider.async_validate_login('test-password') + + +async def test_verify_login(hass, provider): + """Test we raise if http module not load.""" + hass.http = Mock(api_password='test-password') + provider.async_validate_login('test-password') + hass.http = Mock(api_password='test-password') + with pytest.raises(legacy_api_password.InvalidAuthError): + provider.async_validate_login('invalid-password') + + +async def test_utf_8_username_password(provider): + """Test that we create a new credential.""" + credentials = await provider.async_get_or_create_credentials({ + 'username': '🎉', + 'password': '😎', + }) + assert credentials.is_new is True diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index dd8b2cd35c4..3e5eed4c924 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -1,20 +1,23 @@ """The tests for the Home Assistant HTTP component.""" # pylint: disable=protected-access from ipaddress import ip_network -from unittest.mock import patch +from unittest.mock import patch, Mock +import pytest from aiohttp import BasicAuth, web from aiohttp.web_exceptions import HTTPUnauthorized -import pytest +from homeassistant.auth import AccessToken, RefreshToken +from homeassistant.components.http.auth import setup_auth +from homeassistant.components.http.const import KEY_AUTHENTICATED +from homeassistant.components.http.real_ip import setup_real_ip from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.setup import async_setup_component -from homeassistant.components.http.auth import setup_auth -from homeassistant.components.http.real_ip import setup_real_ip -from homeassistant.components.http.const import KEY_AUTHENTICATED - from . import mock_real_ip + +ACCESS_TOKEN = 'tk.1234' + API_PASSWORD = 'test1234' # Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases @@ -36,15 +39,37 @@ async def mock_handler(request): return web.Response(status=200) +def mock_async_get_access_token(token): + """Return if token is valid.""" + if token == ACCESS_TOKEN: + return Mock(spec=AccessToken, + token=ACCESS_TOKEN, + refresh_token=Mock(spec=RefreshToken)) + else: + return None + + @pytest.fixture def app(): """Fixture to setup a web.Application.""" app = web.Application() + mock_auth = Mock(async_get_access_token=mock_async_get_access_token) + app['hass'] = Mock(auth=mock_auth) app.router.add_get('/', mock_handler) setup_real_ip(app, False, []) return app +@pytest.fixture +def app2(): + """Fixture to setup a web.Application without real_ip middleware.""" + app = web.Application() + mock_auth = Mock(async_get_access_token=mock_async_get_access_token) + app['hass'] = Mock(auth=mock_auth) + app.router.add_get('/', mock_handler) + return app + + async def test_auth_middleware_loaded_by_default(hass): """Test accessing to server from banned IP when feature is off.""" with patch('homeassistant.components.http.setup_auth') as mock_setup: @@ -57,7 +82,7 @@ async def test_auth_middleware_loaded_by_default(hass): async def test_access_without_password(app, aiohttp_client): """Test access without password.""" - setup_auth(app, [], None) + setup_auth(app, [], False, api_password=None) client = await aiohttp_client(app) resp = await client.get('/') @@ -65,8 +90,8 @@ async def test_access_without_password(app, aiohttp_client): async def test_access_with_password_in_header(app, aiohttp_client): - """Test access with password in URL.""" - setup_auth(app, [], API_PASSWORD) + """Test access with password in header.""" + setup_auth(app, [], False, api_password=API_PASSWORD) client = await aiohttp_client(app) req = await client.get( @@ -79,8 +104,8 @@ async def test_access_with_password_in_header(app, aiohttp_client): async def test_access_with_password_in_query(app, aiohttp_client): - """Test access without password.""" - setup_auth(app, [], API_PASSWORD) + """Test access with password in URL.""" + setup_auth(app, [], False, api_password=API_PASSWORD) client = await aiohttp_client(app) resp = await client.get('/', params={ @@ -99,7 +124,7 @@ async def test_access_with_password_in_query(app, aiohttp_client): async def test_basic_auth_works(app, aiohttp_client): """Test access with basic authentication.""" - setup_auth(app, [], API_PASSWORD) + setup_auth(app, [], False, api_password=API_PASSWORD) client = await aiohttp_client(app) req = await client.get( @@ -125,15 +150,12 @@ async def test_basic_auth_works(app, aiohttp_client): assert req.status == 401 -async def test_access_with_trusted_ip(aiohttp_client): +async def test_access_with_trusted_ip(app2, aiohttp_client): """Test access with an untrusted ip address.""" - app = web.Application() - app.router.add_get('/', mock_handler) + setup_auth(app2, TRUSTED_NETWORKS, False, api_password='some-pass') - setup_auth(app, TRUSTED_NETWORKS, 'some-pass') - - set_mock_ip = mock_real_ip(app) - client = await aiohttp_client(app) + set_mock_ip = mock_real_ip(app2) + client = await aiohttp_client(app2) for remote_addr in UNTRUSTED_ADDRESSES: set_mock_ip(remote_addr) @@ -146,3 +168,94 @@ async def test_access_with_trusted_ip(aiohttp_client): resp = await client.get('/') assert resp.status == 200, \ "{} should be trusted".format(remote_addr) + + +async def test_auth_active_access_with_access_token_in_header( + app, aiohttp_client): + """Test access with access token in header.""" + setup_auth(app, [], True, api_password=None) + client = await aiohttp_client(app) + + req = await client.get( + '/', headers={'Authorization': 'Bearer {}'.format(ACCESS_TOKEN)}) + assert req.status == 200 + + req = await client.get( + '/', headers={'AUTHORIZATION': 'Bearer {}'.format(ACCESS_TOKEN)}) + assert req.status == 200 + + req = await client.get( + '/', headers={'authorization': 'Bearer {}'.format(ACCESS_TOKEN)}) + assert req.status == 200 + + req = await client.get( + '/', headers={'Authorization': ACCESS_TOKEN}) + assert req.status == 401 + + req = await client.get( + '/', headers={'Authorization': 'BEARER {}'.format(ACCESS_TOKEN)}) + assert req.status == 401 + + req = await client.get( + '/', headers={'Authorization': 'Bearer wrong-pass'}) + assert req.status == 401 + + +async def test_auth_active_access_with_trusted_ip(app2, aiohttp_client): + """Test access with an untrusted ip address.""" + setup_auth(app2, TRUSTED_NETWORKS, True, api_password=None) + + set_mock_ip = mock_real_ip(app2) + client = await aiohttp_client(app2) + + for remote_addr in UNTRUSTED_ADDRESSES: + set_mock_ip(remote_addr) + resp = await client.get('/') + assert resp.status == 401, \ + "{} shouldn't be trusted".format(remote_addr) + + for remote_addr in TRUSTED_ADDRESSES: + set_mock_ip(remote_addr) + resp = await client.get('/') + assert resp.status == 200, \ + "{} should be trusted".format(remote_addr) + + +async def test_auth_active_blocked_api_password_access(app, aiohttp_client): + """Test access using api_password should be blocked when auth.active.""" + setup_auth(app, [], True, api_password=API_PASSWORD) + client = await aiohttp_client(app) + + req = await client.get( + '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) + assert req.status == 401 + + resp = await client.get('/', params={ + 'api_password': API_PASSWORD + }) + assert resp.status == 401 + + req = await client.get( + '/', + auth=BasicAuth('homeassistant', API_PASSWORD)) + assert req.status == 401 + + +async def test_auth_legacy_support_api_password_access(app, aiohttp_client): + """Test access using api_password if auth.support_legacy.""" + setup_auth(app, [], True, support_legacy=True, api_password=API_PASSWORD) + client = await aiohttp_client(app) + + req = await client.get( + '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) + assert req.status == 200 + + resp = await client.get('/', params={ + 'api_password': API_PASSWORD + }) + assert resp.status == 200 + + req = await client.get( + '/', + auth=BasicAuth('homeassistant', API_PASSWORD)) + assert req.status == 200 diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index fbd8584a7d1..6ea90bcdb88 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -77,7 +77,7 @@ def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): assert mock_process_wrong_login.called assert msg['type'] == wapi.TYPE_AUTH_INVALID - assert msg['message'] == 'Invalid password' + assert msg['message'] == 'Invalid access token or password' @asyncio.coroutine @@ -316,47 +316,103 @@ def test_unknown_command(websocket_client): assert msg['error']['code'] == wapi.ERR_UNKNOWN_COMMAND -async def test_auth_with_token(hass, aiohttp_client, hass_access_token): +async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): """Test authenticating with a token.""" assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) + 'http': { + 'api_password': API_PASSWORD + } + }) client = await aiohttp_client(hass.http.app) async with client.ws_connect(wapi.URL) as ws: - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token.token - }) + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': hass_access_token.token + }) - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK + + +async def test_auth_active_with_password_not_allow(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active', + return_value=True): + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'api_password': API_PASSWORD + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID + + +async def test_auth_legacy_support_with_password(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + with patch('homeassistant.auth.AuthManager.active', + return_value=True),\ + patch('homeassistant.auth.AuthManager.support_legacy', + return_value=True): + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'api_password': API_PASSWORD + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK async def test_auth_with_invalid_token(hass, aiohttp_client): """Test authenticating with a token.""" assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) + 'http': { + 'api_password': API_PASSWORD + } + }) client = await aiohttp_client(hass.http.app) async with client.ws_connect(wapi.URL) as ws: - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': 'incorrect' - }) + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': 'incorrect' + }) - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID From 3c3a53a137518e6bc31c419b2c24d8384ea16e70 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Jul 2018 08:53:33 -0400 Subject: [PATCH 117/128] Update translations --- .../components/cast/.translations/cs.json | 15 +++++++++ .../components/cast/.translations/de.json | 14 ++++++++ .../components/cast/.translations/hu.json | 14 ++++++++ .../components/cast/.translations/it.json | 15 +++++++++ .../components/cast/.translations/lb.json | 15 +++++++++ .../components/cast/.translations/nl.json | 15 +++++++++ .../components/cast/.translations/sl.json | 15 +++++++++ .../cast/.translations/zh-Hant.json | 15 +++++++++ .../components/deconz/.translations/cs.json | 3 +- .../components/deconz/.translations/de.json | 8 ++++- .../components/deconz/.translations/lb.json | 3 +- .../components/deconz/.translations/nl.json | 7 ++++ .../components/deconz/.translations/sl.json | 3 +- .../deconz/.translations/zh-Hant.json | 3 +- .../components/hue/.translations/de.json | 2 +- .../components/hue/.translations/ru.json | 2 +- .../components/nest/.translations/cs.json | 33 +++++++++++++++++++ .../components/nest/.translations/de.json | 21 ++++++++++++ .../components/nest/.translations/hu.json | 20 +++++++++++ .../components/nest/.translations/it.json | 17 ++++++++++ .../components/nest/.translations/lb.json | 33 +++++++++++++++++++ .../components/nest/.translations/nl.json | 33 +++++++++++++++++++ .../components/nest/.translations/sl.json | 33 +++++++++++++++++++ .../nest/.translations/zh-Hant.json | 33 +++++++++++++++++++ .../components/sonos/.translations/cs.json | 15 +++++++++ .../components/sonos/.translations/de.json | 14 ++++++++ .../components/sonos/.translations/hu.json | 14 ++++++++ .../components/sonos/.translations/it.json | 15 +++++++++ .../components/sonos/.translations/lb.json | 15 +++++++++ .../components/sonos/.translations/nl.json | 15 +++++++++ .../components/sonos/.translations/sl.json | 15 +++++++++ .../sonos/.translations/zh-Hant.json | 15 +++++++++ script/translations_download | 2 +- 33 files changed, 484 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/cast/.translations/cs.json create mode 100644 homeassistant/components/cast/.translations/de.json create mode 100644 homeassistant/components/cast/.translations/hu.json create mode 100644 homeassistant/components/cast/.translations/it.json create mode 100644 homeassistant/components/cast/.translations/lb.json create mode 100644 homeassistant/components/cast/.translations/nl.json create mode 100644 homeassistant/components/cast/.translations/sl.json create mode 100644 homeassistant/components/cast/.translations/zh-Hant.json create mode 100644 homeassistant/components/nest/.translations/cs.json create mode 100644 homeassistant/components/nest/.translations/de.json create mode 100644 homeassistant/components/nest/.translations/hu.json create mode 100644 homeassistant/components/nest/.translations/it.json create mode 100644 homeassistant/components/nest/.translations/lb.json create mode 100644 homeassistant/components/nest/.translations/nl.json create mode 100644 homeassistant/components/nest/.translations/sl.json create mode 100644 homeassistant/components/nest/.translations/zh-Hant.json create mode 100644 homeassistant/components/sonos/.translations/cs.json create mode 100644 homeassistant/components/sonos/.translations/de.json create mode 100644 homeassistant/components/sonos/.translations/hu.json create mode 100644 homeassistant/components/sonos/.translations/it.json create mode 100644 homeassistant/components/sonos/.translations/lb.json create mode 100644 homeassistant/components/sonos/.translations/nl.json create mode 100644 homeassistant/components/sonos/.translations/sl.json create mode 100644 homeassistant/components/sonos/.translations/zh-Hant.json diff --git a/homeassistant/components/cast/.translations/cs.json b/homeassistant/components/cast/.translations/cs.json new file mode 100644 index 00000000000..82f063b365f --- /dev/null +++ b/homeassistant/components/cast/.translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyly nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed Google Cast.", + "single_instance_allowed": "Pouze jedin\u00e1 konfigurace Google Cast je nezbytn\u00e1." + }, + "step": { + "confirm": { + "description": "Chcete nastavit Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/de.json b/homeassistant/components/cast/.translations/de.json new file mode 100644 index 00000000000..2572c3344eb --- /dev/null +++ b/homeassistant/components/cast/.translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Google Cast Ger\u00e4te im Netzwerk gefunden." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie Google Cast einrichten?", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/hu.json b/homeassistant/components/cast/.translations/hu.json new file mode 100644 index 00000000000..f59a1b43ef1 --- /dev/null +++ b/homeassistant/components/cast/.translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3k Google Cast eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton." + }, + "step": { + "confirm": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Google Cast szolg\u00e1ltat\u00e1st?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/it.json b/homeassistant/components/cast/.translations/it.json new file mode 100644 index 00000000000..21c8e60518e --- /dev/null +++ b/homeassistant/components/cast/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo Google Cast trovato in rete.", + "single_instance_allowed": "\u00c8 necessaria una sola configurazione di Google Cast." + }, + "step": { + "confirm": { + "description": "Vuoi configurare Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/lb.json b/homeassistant/components/cast/.translations/lb.json new file mode 100644 index 00000000000..f1daff83069 --- /dev/null +++ b/homeassistant/components/cast/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng Google Cast Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun Google Cast ass n\u00e9ideg." + }, + "step": { + "confirm": { + "description": "Soll Google Cast konfigur\u00e9iert ginn?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/nl.json b/homeassistant/components/cast/.translations/nl.json new file mode 100644 index 00000000000..91c428770f5 --- /dev/null +++ b/homeassistant/components/cast/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen Google Cast-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Google Cast nodig." + }, + "step": { + "confirm": { + "description": "Wilt u Google Cast instellen?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/sl.json b/homeassistant/components/cast/.translations/sl.json new file mode 100644 index 00000000000..24a7215574d --- /dev/null +++ b/homeassistant/components/cast/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju niso najdene naprave Google Cast.", + "single_instance_allowed": "Potrebna je samo ena konfiguracija Google Cast-a." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti Google Cast?", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/zh-Hant.json b/homeassistant/components/cast/.translations/zh-Hant.json new file mode 100644 index 00000000000..711ac320397 --- /dev/null +++ b/homeassistant/components/cast/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Google Cast \u5373\u53ef\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Google Cast\uff1f", + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json index 0721cac3321..1588766e406 100644 --- a/homeassistant/components/deconz/.translations/cs.json +++ b/homeassistant/components/deconz/.translations/cs.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel" + "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel", + "allow_deconz_groups": "Povolit import skupin deCONZ " }, "title": "Dal\u0161\u00ed mo\u017enosti konfigurace pro deCONZ" } diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 9d3dc9e6e62..b09b7e15b31 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -19,8 +19,14 @@ "link": { "description": "Entsperren Sie Ihr deCONZ-Gateway, um sich bei Home Assistant zu registrieren. \n\n 1. Gehen Sie zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccken Sie die Taste \"Gateway entsperren\"", "title": "Mit deCONZ verbinden" + }, + "options": { + "data": { + "allow_clip_sensor": "Import virtueller Sensoren zulassen", + "allow_deconz_groups": "Import von deCONZ-Gruppen zulassen" + } } }, - "title": "deCONZ" + "title": "deCONZ Zigbee Gateway" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 46190d23926..3de7de9ddb3 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren" + "allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren", + "allow_deconz_groups": "Erlaabt den Import vun deCONZ Gruppen" }, "title": "Extra Konfiguratiouns Optiounen fir deCONZ" } diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index 90d13bb39b4..6f3fa2ec9a4 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -19,6 +19,13 @@ "link": { "description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen\n2. Druk op de knop \"Gateway ontgrendelen\"", "title": "Koppel met deCONZ" + }, + "options": { + "data": { + "allow_clip_sensor": "Sta het importeren van virtuele sensoren toe", + "allow_deconz_groups": "Sta de import van deCONZ-groepen toe" + }, + "title": "Extra configuratieopties voor deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 59c5577c96b..bc7a2cbd861 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev" + "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev", + "allow_deconz_groups": "Dovoli uvoz deCONZ skupin" }, "title": "Dodatne mo\u017enosti konfiguracije za deCONZ" } diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 17cbe87f1e8..5cd1a14d499 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -22,7 +22,8 @@ }, "options": { "data": { - "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668" + "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668", + "allow_deconz_groups": "\u5141\u8a31\u532f\u5165 deCONZ \u7fa4\u7d44" }, "title": "deCONZ \u9644\u52a0\u8a2d\u5b9a\u9078\u9805" } diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index d466488e9fc..dc0968dc88a 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -24,6 +24,6 @@ "title": "Hub verbinden" } }, - "title": "Philips Hue Bridge" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index ea1e4fff1bf..b471dd1a0cd 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -24,6 +24,6 @@ "title": "\u0421\u0432\u044f\u0437\u044c \u0441 \u0445\u0430\u0431\u043e\u043c" } }, - "title": "\u0428\u043b\u044e\u0437 Philips Hue" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/cs.json b/homeassistant/components/nest/.translations/cs.json new file mode 100644 index 00000000000..c884226174b --- /dev/null +++ b/homeassistant/components/nest/.translations/cs.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "M\u016f\u017eete nastavit pouze jeden Nest \u00fa\u010det.", + "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL.", + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00ed URL vypr\u0161el", + "no_flows": "Pot\u0159ebujete nakonfigurovat Nest, abyste se s n\u00edm mohli autentizovat. [P\u0159e\u010dt\u011bte si pros\u00edm pokyny] (https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Intern\u00ed chyba ov\u011b\u0159en\u00ed k\u00f3du", + "invalid_code": "Neplatn\u00fd k\u00f3d", + "timeout": "\u010casov\u00fd limit ov\u011b\u0159ov\u00e1n\u00ed k\u00f3du vypr\u0161el", + "unknown": "Nezn\u00e1m\u00e1 chyba ov\u011b\u0159en\u00ed k\u00f3du" + }, + "step": { + "init": { + "data": { + "flow_impl": "Poskytovatel" + }, + "description": "Zvolte pomoc\u00ed kter\u00e9ho poskytovatele ov\u011b\u0159en\u00ed chcete ov\u011b\u0159it slu\u017ebu Nest.", + "title": "Poskytovatel ov\u011b\u0159en\u00ed" + }, + "link": { + "data": { + "code": "K\u00f3d PIN" + }, + "description": "Chcete-li propojit \u00fa\u010det Nest, [autorizujte sv\u016fj \u00fa\u010det]({url}). \n\n Po autorizaci zkop\u00edrujte n\u00ed\u017ee uveden\u00fd k\u00f3d PIN.", + "title": "Propojit s Nest \u00fa\u010dtem" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/de.json b/homeassistant/components/nest/.translations/de.json new file mode 100644 index 00000000000..721eafa807f --- /dev/null +++ b/homeassistant/components/nest/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "init": { + "data": { + "flow_impl": "Anbieter" + }, + "description": "W\u00e4hlen Sie, \u00fcber welchen Authentifizierungsanbieter Sie sich bei Nest authentifizieren m\u00f6chten.", + "title": "Authentifizierungsanbieter" + }, + "link": { + "data": { + "code": "PIN Code" + }, + "description": "[Autorisieren Sie ihr Konto] ( {url} ), um ihren Nest-Account zu verkn\u00fcpfen.\n\n F\u00fcgen Sie anschlie\u00dfend den erhaltenen PIN Code hier ein.", + "title": "Nest-Konto verkn\u00fcpfen" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/hu.json b/homeassistant/components/nest/.translations/hu.json new file mode 100644 index 00000000000..abf8f79599f --- /dev/null +++ b/homeassistant/components/nest/.translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d" + }, + "step": { + "init": { + "data": { + "flow_impl": "Szolg\u00e1ltat\u00f3" + } + }, + "link": { + "data": { + "code": "PIN-k\u00f3d" + } + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/it.json b/homeassistant/components/nest/.translations/it.json new file mode 100644 index 00000000000..ca34179cf5b --- /dev/null +++ b/homeassistant/components/nest/.translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "init": { + "title": "Fornitore di autenticazione" + }, + "link": { + "data": { + "code": "Codice PIN" + }, + "description": "Per collegare l'account Nido, [autorizzare l'account]({url}).\n\nDopo l'autorizzazione, copia-incolla il codice PIN fornito di seguito.", + "title": "Collega un account Nest" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/lb.json b/homeassistant/components/nest/.translations/lb.json new file mode 100644 index 00000000000..197cc8206d0 --- /dev/null +++ b/homeassistant/components/nest/.translations/lb.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen\u00a0Nest Kont\u00a0konfigur\u00e9ieren.", + "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", + "authorize_url_timeout": "Z\u00e4it Iwwerschreidung\u00a0beim gener\u00e9ieren\u00a0vun der Autorisatiouns\u00a0URL.", + "no_flows": "Dir musst Nest konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung\u00a0k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Interne Feeler beim valid\u00e9ieren vum Code", + "invalid_code": "Ong\u00ebltege Code", + "timeout": "Z\u00e4it Iwwerschreidung\u00a0beim valid\u00e9ieren vum Code", + "unknown": "Onbekannte Feeler beim valid\u00e9ieren vum Code" + }, + "step": { + "init": { + "data": { + "flow_impl": "Ubidder" + }, + "description": "Wielt den Authentifikatioun Ubidder deen sech mat Nest verbanne soll.", + "title": "Authentifikatioun Ubidder" + }, + "link": { + "data": { + "code": "Pin code" + }, + "description": "Fir den Nest Kont ze verbannen, [autoris\u00e9iert \u00e4ren Kont]({url}).\nKop\u00e9iert no der Autorisatioun den Pin hei \u00ebnnendr\u00ebnner", + "title": "Nest Kont verbannen" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/nl.json b/homeassistant/components/nest/.translations/nl.json new file mode 100644 index 00000000000..756eb07189a --- /dev/null +++ b/homeassistant/components/nest/.translations/nl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Je kunt slechts \u00e9\u00e9n Nest-account configureren.", + "authorize_url_fail": "Onbekende fout bij het genereren van een autoriseer-URL.", + "authorize_url_timeout": "Toestemming voor het genereren van autoriseer-url.", + "no_flows": "U moet Nest configureren voordat u zich ermee kunt authenticeren. [Gelieve de instructies te lezen](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Interne foutvalidatiecode", + "invalid_code": "Ongeldige code", + "timeout": "Time-out validatie van code", + "unknown": "Onbekende foutvalidatiecode" + }, + "step": { + "init": { + "data": { + "flow_impl": "Leverancier" + }, + "description": "Kies met welke authenticatieleverancier u wilt verifi\u00ebren met Nest.", + "title": "Authenticatieleverancier" + }, + "link": { + "data": { + "code": "Pincode" + }, + "description": "Als je je Nest-account wilt koppelen, [autoriseer je account] ( {url} ). \n\nNa autorisatie, kopieer en plak de voorziene pincode hieronder.", + "title": "Koppel Nest-account" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/sl.json b/homeassistant/components/nest/.translations/sl.json new file mode 100644 index 00000000000..d038ed4157f --- /dev/null +++ b/homeassistant/components/nest/.translations/sl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "Nastavite lahko samo en ra\u010dun Nest.", + "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "no_flows": "Preden lahko preverite pristnost, morate konfigurirati Nest. [Preberite navodila](https://www.home-assistant.io/components/nest/)." + }, + "error": { + "internal_error": "Notranja napaka pri preverjanju kode", + "invalid_code": "Neveljavna koda", + "timeout": "\u010casovna omejitev je potekla pri preverjanju kode", + "unknown": "Neznana napaka pri preverjanju kode" + }, + "step": { + "init": { + "data": { + "flow_impl": "Ponudnik" + }, + "description": "Izberite prek katerega ponudnika overjanja \u017eelite overiti Nest.", + "title": "Ponudnik za preverjanje pristnosti" + }, + "link": { + "data": { + "code": "PIN koda" + }, + "description": "\u010ce \u017eelite povezati svoj ra\u010dun Nest, [pooblastite svoj ra\u010dun]({url}). \n\n Po odobritvi kopirajte in prilepite podano kodo PIN.", + "title": "Pove\u017eite Nest ra\u010dun" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/zh-Hant.json b/homeassistant/components/nest/.translations/zh-Hant.json new file mode 100644 index 00000000000..6b9dbdb19b1 --- /dev/null +++ b/homeassistant/components/nest/.translations/zh-Hant.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Nest \u5e33\u865f\u3002", + "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", + "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Nest \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/nest/\uff09\u3002" + }, + "error": { + "internal_error": "\u8a8d\u8b49\u78bc\u5167\u90e8\u932f\u8aa4", + "invalid_code": "\u8a8d\u8b49\u78bc\u7121\u6548", + "timeout": "\u8a8d\u8b49\u78bc\u903e\u6642", + "unknown": "\u8a8d\u8b49\u78bc\u672a\u77e5\u932f\u8aa4" + }, + "step": { + "init": { + "data": { + "flow_impl": "\u8a8d\u8b49\u63d0\u4f9b\u8005" + }, + "description": "\u65bc\u8a8d\u8b49\u63d0\u4f9b\u8005\u4e2d\u6311\u9078\u6240\u8981\u9032\u884c Nest \u8a8d\u8b49\u63d0\u4f9b\u8005\u3002", + "title": "\u8a8d\u8b49\u63d0\u4f9b\u8005" + }, + "link": { + "data": { + "code": "PIN \u78bc" + }, + "description": "\u6b32\u9023\u7d50 Nest \u5e33\u865f\uff0c[\u8a8d\u8b49\u5e33\u865f]({url}).\n\n\u65bc\u8a8d\u8b49\u5f8c\uff0c\u8907\u88fd\u4e26\u8cbc\u4e0a\u4e0b\u65b9\u7684\u8a8d\u8b49\u78bc\u3002", + "title": "\u9023\u7d50 Nest \u5e33\u865f" + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/cs.json b/homeassistant/components/sonos/.translations/cs.json new file mode 100644 index 00000000000..c0b26284cdf --- /dev/null +++ b/homeassistant/components/sonos/.translations/cs.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyly nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed Sonos.", + "single_instance_allowed": "Je t\u0159eba jen jedna konfigurace Sonos." + }, + "step": { + "confirm": { + "description": "Chcete nastavit Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/de.json b/homeassistant/components/sonos/.translations/de.json new file mode 100644 index 00000000000..f1b76b0d155 --- /dev/null +++ b/homeassistant/components/sonos/.translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Sonos Ger\u00e4te im Netzwerk gefunden." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie Sonos konfigurieren?", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/hu.json b/homeassistant/components/sonos/.translations/hu.json new file mode 100644 index 00000000000..4726d57ad24 --- /dev/null +++ b/homeassistant/components/sonos/.translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3k Sonos eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton." + }, + "step": { + "confirm": { + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Sonos-t?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/it.json b/homeassistant/components/sonos/.translations/it.json new file mode 100644 index 00000000000..e32557f1d95 --- /dev/null +++ b/homeassistant/components/sonos/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Non sono presenti dispositivi Sonos in rete.", + "single_instance_allowed": "\u00c8 necessaria una sola configurazione di Sonos." + }, + "step": { + "confirm": { + "description": "Vuoi installare Sonos", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/lb.json b/homeassistant/components/sonos/.translations/lb.json new file mode 100644 index 00000000000..26eaec4584d --- /dev/null +++ b/homeassistant/components/sonos/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng Sonos Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun Sonos ass n\u00e9ideg." + }, + "step": { + "confirm": { + "description": "Soll Sonos konfigur\u00e9iert ginn?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/nl.json b/homeassistant/components/sonos/.translations/nl.json new file mode 100644 index 00000000000..de84482cc63 --- /dev/null +++ b/homeassistant/components/sonos/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen Sonos-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van Sonos nodig." + }, + "step": { + "confirm": { + "description": "Wilt u Sonos instellen?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/sl.json b/homeassistant/components/sonos/.translations/sl.json new file mode 100644 index 00000000000..6773465bbbf --- /dev/null +++ b/homeassistant/components/sonos/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni najdenih naprav Sonos.", + "single_instance_allowed": "Potrebna je samo ena konfiguracija Sonosa." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti Sonos?", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/zh-Hant.json b/homeassistant/components/sonos/.translations/zh-Hant.json new file mode 100644 index 00000000000..c6fb13c3605 --- /dev/null +++ b/homeassistant/components/sonos/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Sonos \u5373\u53ef\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Sonos\uff1f", + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/script/translations_download b/script/translations_download index 099e32c9d1b..15b6a681056 100755 --- a/script/translations_download +++ b/script/translations_download @@ -28,7 +28,7 @@ mkdir -p ${LOCAL_DIR} docker run \ -v ${LOCAL_DIR}:/opt/dest/locale \ - lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7 lokalise \ + lokalise/lokalise-cli@sha256:ddf5677f58551261008342df5849731c88bcdc152ab645b133b21819aede8218 lokalise \ --token ${LOKALISE_TOKEN} \ export ${PROJECT_ID} \ --export_empty skip \ From 855cbc0aed368244eef019153e02f87df486dc81 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Jul 2018 08:56:37 -0400 Subject: [PATCH 118/128] Update frontend to 20180702.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 9a32626c66a..b916b794936 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180701.0'] +REQUIREMENTS = ['home-assistant-frontend==20180702.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index b011bd6747e..30e0e39aa73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180701.0 +home-assistant-frontend==20180702.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 124fdc736a8..cea28b3e7ec 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==20180701.0 +home-assistant-frontend==20180702.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From c39e6b9618f353a1abc9f0cd09e6db74acca2d3c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Jul 2018 08:57:26 -0400 Subject: [PATCH 119/128] Bumped version to 0.73.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6fd41b5f4d2..8bf3ca3ff89 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 73 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 0dc155c4d3b7742c5bfbdeef3a4c6367626b7dc4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Jul 2018 14:43:31 -0400 Subject: [PATCH 120/128] Bump frontend to 20180702.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 b916b794936..25859020be4 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180702.0'] +REQUIREMENTS = ['home-assistant-frontend==20180702.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 30e0e39aa73..eb1951b368d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180702.0 +home-assistant-frontend==20180702.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cea28b3e7ec..473d9ca9b17 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==20180702.0 +home-assistant-frontend==20180702.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From d7fd9247a996653d9298a35750fb2241ca972861 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Jul 2018 14:44:15 -0400 Subject: [PATCH 121/128] Bumped version to 0.73.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8bf3ca3ff89..41256130600 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 73 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 1c525968d1fbea3ebda2c722d9acd9c52152c2df Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Jul 2018 11:03:23 -0400 Subject: [PATCH 122/128] Bump frontend to 20180703.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 25859020be4..cb5f06f12ed 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180702.1'] +REQUIREMENTS = ['home-assistant-frontend==20180703.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index eb1951b368d..cc85b8fdf09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180702.1 +home-assistant-frontend==20180703.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 473d9ca9b17..750e7a03e60 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==20180702.1 +home-assistant-frontend==20180703.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From b82371f44b91f82daa87166d8d0f97eb6b831a26 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Jul 2018 11:11:14 -0400 Subject: [PATCH 123/128] Bumped version to 0.73.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 41256130600..f2df6fb3236 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 73 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0b4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From cb458b774523964b0f9854fce5880135f70f3c1b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Jul 2018 14:51:57 -0400 Subject: [PATCH 124/128] Bump frontend to 20180703.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 cb5f06f12ed..d74aadd3323 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180703.0'] +REQUIREMENTS = ['home-assistant-frontend==20180703.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index cc85b8fdf09..14cc3ab6e07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180703.0 +home-assistant-frontend==20180703.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 750e7a03e60..175c380f099 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==20180703.0 +home-assistant-frontend==20180703.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 07dde62e7082fde9089409daf6c917fd06e35f8f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Jul 2018 14:58:31 -0400 Subject: [PATCH 125/128] Bumped version to 0.73.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f2df6fb3236..9d3ead609d9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 73 -PATCH_VERSION = '0b4' +PATCH_VERSION = '0b5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 852526e10a9c072ad9c247557401edbff01f8090 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Jul 2018 12:11:18 -0400 Subject: [PATCH 126/128] Bump frontend to 20180704.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 d74aadd3323..0b9c8edd411 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from homeassistant.util.yaml import load_yaml -REQUIREMENTS = ['home-assistant-frontend==20180703.1'] +REQUIREMENTS = ['home-assistant-frontend==20180704.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 14cc3ab6e07..c93c417b111 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -415,7 +415,7 @@ hole==0.3.0 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180703.1 +home-assistant-frontend==20180704.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 175c380f099..f0c64b63147 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==20180703.1 +home-assistant-frontend==20180704.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 46de89e1a3f5b7f9d5d900968ff6b15ca42a33af Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Jul 2018 12:11:52 -0400 Subject: [PATCH 127/128] Bumped version to 0.73.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9d3ead609d9..150c137af5f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 73 -PATCH_VERSION = '0b5' +PATCH_VERSION = '0b6' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 1e7cfc04af2e6b8824c62c7ed7ef6b57cd59060d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 6 Jul 2018 22:31:09 +0200 Subject: [PATCH 128/128] Bumped version to 0.73.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 150c137af5f..57c1bccbd6a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 73 -PATCH_VERSION = '0b6' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)