From 801401e9cb2de9ca01dd8ff9009a33ebae71f239 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Feb 2019 20:47:04 -0800 Subject: [PATCH 001/253] Bumped version to 0.89.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1a3d4e2e455..56e174effdf 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 = 88 +MINOR_VERSION = 89 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From f4b2573c4b11aa81ca74b1c5311b26d68b57ea8d Mon Sep 17 00:00:00 2001 From: Victor Cerutti Date: Thu, 14 Feb 2019 14:40:27 +0100 Subject: [PATCH 002/253] =?UTF-8?q?M=C3=A9t=C3=A9o-France=20platform=20for?= =?UTF-8?q?=20the=20weather=20component=20(#18404)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * new weather component for meteofrance * linting * upgrade meteofrance package version * Update .coveragerc * Remove updates to the weather component architecture * Rewrite Météo-France as a component * Update .coveragerc * Update requirements_all.txt * remove Weather Card option * Update conf name Changing conf name to something more universal for worldwide weather forecast (postal code was only relevent for France) * Update meteofrance pypi package * fix line too long * Update requirements_all.txt * prevent from calling an API endpoint if not in monitored conditions * fix stale url and remove blank line * Insure that all cities are unique * rename CONF_ATTRIBUTION * Updating data from component setup * fix missing extra lines --- .coveragerc | 2 + homeassistant/components/meteo_france.py | 141 ++++++++++++++++++ .../components/sensor/meteo_france.py | 77 ++-------- .../components/weather/meteo_france.py | 112 ++++++++++++++ requirements_all.txt | 4 +- 5 files changed, 270 insertions(+), 66 deletions(-) create mode 100644 homeassistant/components/meteo_france.py create mode 100644 homeassistant/components/weather/meteo_france.py diff --git a/.coveragerc b/.coveragerc index 5931322f80b..1302ee911a3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -326,6 +326,7 @@ omit = homeassistant/components/nest/* homeassistant/components/netatmo/* homeassistant/components/netgear_lte/* + homeassistant/components/meteo_france.py homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sqs.py @@ -650,6 +651,7 @@ omit = homeassistant/components/weather/buienradar.py homeassistant/components/weather/darksky.py homeassistant/components/weather/met.py + homeassistant/components/weather/meteo_france.py homeassistant/components/weather/metoffice.py homeassistant/components/weather/openweathermap.py homeassistant/components/weather/zamg.py diff --git a/homeassistant/components/meteo_france.py b/homeassistant/components/meteo_france.py new file mode 100644 index 00000000000..fa68021d91c --- /dev/null +++ b/homeassistant/components/meteo_france.py @@ -0,0 +1,141 @@ +""" +Support for Meteo France weather forecast. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/meteo_france/ +""" +import logging +import datetime + +import voluptuous as vol + +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, TEMP_CELSIUS) +from homeassistant.util import Throttle +from homeassistant.helpers.discovery import load_platform +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['meteofrance==0.3.4'] +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'meteo_france' +SCAN_INTERVAL = datetime.timedelta(minutes=5) +ATTRIBUTION = "Data provided by Météo-France" +CONF_CITY = 'city' +DEFAULT_WEATHER_CARD = True +DATA_METEO_FRANCE = 'data_meteo_france' + +SENSOR_TYPES = { + 'rain_chance': ['Rain chance', '%'], + 'freeze_chance': ['Freeze chance', '%'], + 'thunder_chance': ['Thunder chance', '%'], + 'snow_chance': ['Snow chance', '%'], + 'weather': ['Weather', None], + 'wind_speed': ['Wind Speed', 'km/h'], + 'next_rain': ['Next rain', 'min'], + 'temperature': ['Temperature', TEMP_CELSIUS], + 'uv': ['UV', None], +} + +CONDITION_CLASSES = { + 'clear-night': ['Nuit Claire'], + 'cloudy': ['Très nuageux'], + 'fog': ['Brume ou bancs de brouillard', + 'Brouillard', 'Brouillard givrant'], + 'hail': ['Risque de grêle'], + 'lightning': ["Risque d'orages", 'Orages'], + 'lightning-rainy': ['Pluie orageuses', 'Pluies orageuses', + 'Averses orageuses'], + 'partlycloudy': ['Ciel voilé', 'Ciel voilé nuit', 'Éclaircies'], + 'pouring': ['Pluie forte'], + 'rainy': ['Bruine / Pluie faible', 'Bruine', 'Pluie faible', + 'Pluies éparses / Rares averses', 'Pluies éparses', + 'Rares averses', 'Pluie / Averses', 'Averses', 'Pluie'], + 'snowy': ['Neige / Averses de neige', 'Neige', 'Averses de neige', + 'Neige forte', 'Quelques flocons'], + 'snowy-rainy': ['Pluie et neige', 'Pluie verglaçante'], + 'sunny': ['Ensoleillé'], + 'windy': [], + 'windy-variant': [], + 'exceptional': [], +} + + +def has_all_unique_cities(value): + """Validate that all cities are unique.""" + cities = [location[CONF_CITY] for location in value] + vol.Schema(vol.Unique())(cities) + return value + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_CITY): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + })], has_all_unique_cities) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Meteo-France component.""" + hass.data[DATA_METEO_FRANCE] = {} + + for location in config[DOMAIN]: + + city = location[CONF_CITY] + + from meteofrance.client import meteofranceClient, meteofranceError + + try: + client = meteofranceClient(city) + except meteofranceError as exp: + _LOGGER.error(exp) + return + + client.need_rain_forecast = bool(CONF_MONITORED_CONDITIONS in location + and 'next_rain' in + location[CONF_MONITORED_CONDITIONS]) + + hass.data[DATA_METEO_FRANCE][city] = MeteoFranceUpdater(client) + hass.data[DATA_METEO_FRANCE][city].update() + + if CONF_MONITORED_CONDITIONS in location: + monitored_conditions = location[CONF_MONITORED_CONDITIONS] + load_platform( + hass, + 'sensor', + DOMAIN, + {CONF_CITY: city, + CONF_MONITORED_CONDITIONS: monitored_conditions}, + config) + + load_platform( + hass, + 'weather', + DOMAIN, + {CONF_CITY: city}, + config) + + return True + + +class MeteoFranceUpdater: + """Update data from Meteo-France.""" + + def __init__(self, client): + """Initialize the data object.""" + self._client = client + + def get_data(self): + """Get the latest data from Meteo-France.""" + return self._client.get_data() + + @Throttle(SCAN_INTERVAL) + def update(self): + """Get the latest data from Meteo-France.""" + from meteofrance.client import meteofranceError + try: + self._client.update() + except meteofranceError as exp: + _LOGGER.error(exp) diff --git a/homeassistant/components/sensor/meteo_france.py b/homeassistant/components/sensor/meteo_france.py index 1e18b1518a7..12933c02e81 100644 --- a/homeassistant/components/sensor/meteo_france.py +++ b/homeassistant/components/sensor/meteo_france.py @@ -4,64 +4,34 @@ Support for Meteo France raining forecast. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.meteo_france/ """ - import logging -import datetime -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.meteo_france import (SENSOR_TYPES, + DATA_METEO_FRANCE, + CONF_CITY, + ATTRIBUTION) from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION, TEMP_CELSIUS) + CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION) from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['meteofrance==0.2.7'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by Meteo-France" -CONF_POSTAL_CODE = 'postal_code' - STATE_ATTR_FORECAST = '1h rain forecast' -SCAN_INTERVAL = datetime.timedelta(minutes=5) - -SENSOR_TYPES = { - 'rain_chance': ['Rain chance', '%'], - 'freeze_chance': ['Freeze chance', '%'], - 'thunder_chance': ['Thunder chance', '%'], - 'snow_chance': ['Snow chance', '%'], - 'weather': ['Weather', None], - 'wind_speed': ['Wind Speed', 'km/h'], - 'next_rain': ['Next rain', 'min'], - 'temperature': ['Temperature', TEMP_CELSIUS], - 'uv': ['UV', None], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_POSTAL_CODE): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Meteo-France sensor.""" - postal_code = config[CONF_POSTAL_CODE] - - from meteofrance.client import meteofranceClient, meteofranceError - - try: - meteofrance_client = meteofranceClient(postal_code) - except meteofranceError as exp: - _LOGGER.error(exp) + if discovery_info is None: return - client = MeteoFranceUpdater(meteofrance_client) + city = discovery_info[CONF_CITY] + monitored_conditions = discovery_info[CONF_MONITORED_CONDITIONS] + + client = hass.data[DATA_METEO_FRANCE][city] add_entities([MeteoFranceSensor(variable, client) - for variable in config[CONF_MONITORED_CONDITIONS]], + for variable in monitored_conditions], True) @@ -96,10 +66,10 @@ class MeteoFranceSensor(Entity): }, ** self._data["next_rain_intervals"], **{ - ATTR_ATTRIBUTION: CONF_ATTRIBUTION + ATTR_ATTRIBUTION: ATTRIBUTION } } - return {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + return {ATTR_ATTRIBUTION: ATTRIBUTION} @property def unit_of_measurement(self): @@ -116,24 +86,3 @@ class MeteoFranceSensor(Entity): _LOGGER.error("No condition `%s` for location `%s`", self._condition, self._data["name"]) self._state = None - - -class MeteoFranceUpdater: - """Update data from Meteo-France.""" - - def __init__(self, client): - """Initialize the data object.""" - self._client = client - - def get_data(self): - """Get the latest data from Meteo-France.""" - return self._client.get_data() - - @Throttle(SCAN_INTERVAL) - def update(self): - """Get the latest data from Meteo-France.""" - from meteofrance.client import meteofranceError - try: - self._client.update() - except meteofranceError as exp: - _LOGGER.error(exp) diff --git a/homeassistant/components/weather/meteo_france.py b/homeassistant/components/weather/meteo_france.py new file mode 100644 index 00000000000..a9d11d198f6 --- /dev/null +++ b/homeassistant/components/weather/meteo_france.py @@ -0,0 +1,112 @@ +""" +Support for Meteo france weather service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/weather.meteo_france/ +""" +import logging +from datetime import datetime, timedelta + +from homeassistant.components.meteo_france import (DATA_METEO_FRANCE, + CONDITION_CLASSES, + CONF_CITY, + ATTRIBUTION) +from homeassistant.components.weather import ( + WeatherEntity, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) +from homeassistant.const import TEMP_CELSIUS + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Meteo-France weather platform.""" + if discovery_info is None: + return + + city = discovery_info[CONF_CITY] + + client = hass.data[DATA_METEO_FRANCE][city] + + add_entities([MeteoFranceWeather(client)], True) + + +class MeteoFranceWeather(WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, client): + """Initialise the platform with a data instance and station name.""" + self._client = client + self._data = {} + + def update(self): + """Update current conditions.""" + self._client.update() + self._data = self._client.get_data() + + @property + def name(self): + """Return the name of the sensor.""" + return self._data["name"] + + @property + def condition(self): + """Return the current condition.""" + return self.format_condition(self._data["weather"]) + + @property + def temperature(self): + """Return the platform temperature.""" + return self._data["temperature"] + + @property + def humidity(self): + """Return the platform temperature.""" + return None + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def wind_speed(self): + """Return the wind speed.""" + return self._data["wind_speed"] + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self._data["wind_bearing"] + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def forecast(self): + """Return the forecast.""" + reftime = datetime.now().replace(hour=12, minute=00) + reftime += timedelta(hours=24) + forecast_data = [] + for key in self._data["forecast"]: + value = self._data["forecast"][key] + data_dict = { + ATTR_FORECAST_TIME: reftime.isoformat(), + ATTR_FORECAST_TEMP: int(value['max_temp']), + ATTR_FORECAST_TEMP_LOW: int(value['min_temp']), + ATTR_FORECAST_CONDITION: + self.format_condition(value["weather"]) + } + reftime = reftime + timedelta(hours=24) + forecast_data.append(data_dict) + return forecast_data + + @staticmethod + def format_condition(condition): + """Return condition from dict CONDITION_CLASSES.""" + for key, value in CONDITION_CLASSES.items(): + if condition in value: + return key + return condition diff --git a/requirements_all.txt b/requirements_all.txt index b91036fa1f6..01a2b576e5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,8 +681,8 @@ mbddns==0.1.2 # homeassistant.components.notify.message_bird messagebird==1.2.0 -# homeassistant.components.sensor.meteo_france -meteofrance==0.2.7 +# homeassistant.components.meteo_france +meteofrance==0.3.4 # homeassistant.components.sensor.mfi # homeassistant.components.switch.mfi From 3736120c6a6866b1eed0ecd9df35a437b0a2f894 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 14 Feb 2019 16:01:46 +0100 Subject: [PATCH 003/253] Update file header (#21061) * Update file header * Fix lint issue * Fix lint issue --- .../device_sun_light_trigger/__init__.py | 7 +--- .../components/geo_location/__init__.py | 7 +--- homeassistant/components/geo_location/demo.py | 7 +--- .../geo_location/geo_json_events.py | 11 ++---- .../nsw_rural_fire_service_feed.py | 11 ++---- .../geo_location/usgs_earthquakes_feed.py | 11 ++---- homeassistant/components/geofency/__init__.py | 35 ++++++------------- .../components/geofency/device_tracker.py | 8 ++--- homeassistant/components/goalfeed/__init__.py | 11 ++---- homeassistant/components/google/__init__.py | 27 ++++---------- homeassistant/components/google/calendar.py | 7 +--- homeassistant/components/google/tts.py | 17 ++++----- .../components/google_assistant/__init__.py | 11 ++---- .../components/google_assistant/trait.py | 2 +- .../components/google_domains/__init__.py | 11 ++---- .../components/google_pubsub/__init__.py | 13 +++---- .../components/googlehome/__init__.py | 16 +++------ .../components/googlehome/device_tracker.py | 11 ++---- homeassistant/components/googlehome/sensor.py | 16 +++------ .../components/gpslogger/__init__.py | 26 +++++--------- .../components/gpslogger/device_tracker.py | 7 +--- homeassistant/components/graphite/__init__.py | 15 +++----- .../components/greeneye_monitor/__init__.py | 7 +--- homeassistant/components/group/__init__.py | 7 +--- homeassistant/components/habitica/__init__.py | 7 +--- homeassistant/components/habitica/sensor.py | 26 ++++---------- homeassistant/components/hangouts/__init__.py | 28 ++++++--------- .../components/hangouts/config_flow.py | 3 +- homeassistant/components/hangouts/const.py | 4 +-- .../components/hangouts/hangouts_bot.py | 14 ++++---- homeassistant/components/hangouts/intents.py | 4 +-- homeassistant/components/hangouts/notify.py | 19 ++++------ homeassistant/components/harmony/__init__.py | 6 +--- homeassistant/components/harmony/remote.py | 13 +++---- homeassistant/components/hassio/__init__.py | 14 +++----- homeassistant/components/hassio/auth.py | 10 +++--- homeassistant/components/hassio/discovery.py | 10 +++--- homeassistant/components/hassio/handler.py | 7 +--- homeassistant/components/hassio/http.py | 11 ++---- homeassistant/components/hdmi_cec/__init__.py | 28 ++++++--------- .../components/hdmi_cec/media_player.py | 9 ++--- homeassistant/components/hdmi_cec/switch.py | 13 +++---- homeassistant/components/history/__init__.py | 7 +--- .../components/history_graph/__init__.py | 9 ++--- homeassistant/components/hive/__init__.py | 20 +++++------ .../components/hive/binary_sensor.py | 17 ++++----- homeassistant/components/hive/climate.py | 35 +++++++++++-------- homeassistant/components/hive/light.py | 19 ++++------ homeassistant/components/hive/sensor.py | 26 +++++++------- homeassistant/components/hive/switch.py | 13 +++---- homeassistant/components/hlk_sw16/__init__.py | 7 +--- homeassistant/components/hlk_sw16/switch.py | 11 ++---- homeassistant/components/homekit/__init__.py | 18 ++++------ .../components/homekit/accessories.py | 10 +++--- .../components/homekit/type_covers.py | 6 ++-- homeassistant/components/homekit/type_fans.py | 2 +- .../components/homekit/type_lights.py | 12 +++---- .../components/homekit/type_locks.py | 22 +++++++----- .../components/homekit/type_media_players.py | 14 ++++---- .../homekit/type_security_systems.py | 23 +++++++----- .../components/homekit/type_sensors.py | 7 ++-- .../components/homekit/type_switches.py | 6 ++-- .../components/homekit/type_thermostats.py | 28 +++++++-------- homeassistant/components/homekit/util.py | 2 +- .../components/homekit_controller/__init__.py | 9 ++--- .../homekit_controller/alarm_control_panel.py | 18 ++++------ .../homekit_controller/binary_sensor.py | 11 ++---- .../components/homekit_controller/climate.py | 17 ++++----- .../components/homekit_controller/cover.py | 17 ++++----- .../components/homekit_controller/light.py | 11 ++---- .../components/homekit_controller/lock.py | 21 ++++------- .../components/homekit_controller/switch.py | 11 ++---- .../components/homematic/__init__.py | 9 ++--- .../components/homematic/binary_sensor.py | 7 +--- homeassistant/components/homematic/climate.py | 7 +--- homeassistant/components/homematic/cover.py | 7 +--- homeassistant/components/homematic/light.py | 11 ++---- homeassistant/components/homematic/lock.py | 7 +--- homeassistant/components/homematic/notify.py | 10 +++--- homeassistant/components/homematic/sensor.py | 35 +++++-------------- homeassistant/components/homematic/switch.py | 7 +--- .../components/homematicip_cloud/__init__.py | 7 +--- .../homematicip_cloud/alarm_control_panel.py | 11 ++---- .../homematicip_cloud/binary_sensor.py | 10 ++---- .../components/homematicip_cloud/climate.py | 10 ++---- .../homematicip_cloud/config_flow.py | 6 ++-- .../components/homematicip_cloud/cover.py | 12 ++----- .../components/homematicip_cloud/hap.py | 2 +- .../components/homematicip_cloud/light.py | 7 +--- .../components/homematicip_cloud/sensor.py | 11 ++---- .../components/homematicip_cloud/switch.py | 10 ++---- .../components/homeworks/__init__.py | 16 ++++----- homeassistant/components/homeworks/light.py | 13 +++---- homeassistant/components/http/__init__.py | 14 +++----- homeassistant/components/http/auth.py | 9 +++-- homeassistant/components/http/ban.py | 7 ++-- homeassistant/components/http/cors.py | 9 ++--- .../components/http/data_validator.py | 1 - homeassistant/components/http/real_ip.py | 3 +- homeassistant/components/http/view.py | 16 +++------ .../components/huawei_lte/__init__.py | 8 +---- .../components/huawei_lte/device_tracker.py | 8 +---- homeassistant/components/huawei_lte/notify.py | 8 +---- homeassistant/components/huawei_lte/sensor.py | 12 ++----- homeassistant/components/hue/__init__.py | 7 +--- homeassistant/components/hue/bridge.py | 2 +- homeassistant/components/hue/light.py | 15 +++----- .../components/idteck_prox/__init__.py | 2 +- homeassistant/components/ifttt/__init__.py | 2 +- .../components/ifttt/alarm_control_panel.py | 6 ++-- homeassistant/components/ihc/__init__.py | 9 ++--- homeassistant/components/ihc/binary_sensor.py | 25 +++++-------- homeassistant/components/ihc/light.py | 12 ++----- homeassistant/components/ihc/sensor.py | 15 +++----- homeassistant/components/ihc/switch.py | 11 ++---- homeassistant/components/ios/__init__.py | 15 +++----- homeassistant/components/ios/notify.py | 20 ++++------- homeassistant/components/ios/sensor.py | 7 +--- homeassistant/components/ipma/__init__.py | 8 +---- homeassistant/components/ipma/const.py | 2 +- homeassistant/components/ipma/weather.py | 13 +++---- homeassistant/components/isy994/__init__.py | 14 +++----- .../components/isy994/binary_sensor.py | 19 ++++------ homeassistant/components/isy994/cover.py | 15 +++----- homeassistant/components/isy994/fan.py | 17 ++++----- homeassistant/components/isy994/light.py | 10 ++---- homeassistant/components/isy994/lock.py | 17 ++++----- homeassistant/components/isy994/sensor.py | 14 +++----- homeassistant/components/isy994/switch.py | 13 +++---- homeassistant/components/smartthings/lock.py | 13 +++---- 130 files changed, 511 insertions(+), 1058 deletions(-) diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index 2b047e92c1e..00adefc6b5c 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -1,9 +1,4 @@ -""" -Provides functionality to turn on lights based on the states. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/device_sun_light_trigger/ -""" +"""Support to turn on lights based on the states.""" import logging from datetime import timedelta diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 9095ce617aa..75c99ecc74c 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -1,9 +1,4 @@ -""" -Geolocation component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/geo_location/ -""" +"""Support for Geolocation.""" from datetime import timedelta import logging from typing import Optional diff --git a/homeassistant/components/geo_location/demo.py b/homeassistant/components/geo_location/demo.py index 0e7274e7a0a..523e125a737 100644 --- a/homeassistant/components/geo_location/demo.py +++ b/homeassistant/components/geo_location/demo.py @@ -1,9 +1,4 @@ -""" -Demo platform for the geolocation component. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/demo/ -""" +"""Demo platform for the geolocation component.""" from datetime import timedelta import logging from math import cos, pi, radians, sin diff --git a/homeassistant/components/geo_location/geo_json_events.py b/homeassistant/components/geo_location/geo_json_events.py index cbfe605e722..e89616126d5 100644 --- a/homeassistant/components/geo_location/geo_json_events.py +++ b/homeassistant/components/geo_location/geo_json_events.py @@ -1,9 +1,4 @@ -""" -Generic GeoJSON events platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/geo_location/geo_json_events/ -""" +"""Support for generic GeoJSON events.""" from datetime import timedelta import logging from typing import Optional @@ -13,8 +8,8 @@ import voluptuous as vol from homeassistant.components.geo_location import ( PLATFORM_SCHEMA, GeolocationEvent) from homeassistant.const import ( - CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, EVENT_HOMEASSISTANT_START, - CONF_LATITUDE, CONF_LONGITUDE) + CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, + EVENT_HOMEASSISTANT_START) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( diff --git a/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py index e0974ed415d..38491feb32f 100644 --- a/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py +++ b/homeassistant/components/geo_location/nsw_rural_fire_service_feed.py @@ -1,9 +1,4 @@ -""" -NSW Rural Fire Service Feed platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/geo_location/nsw_rural_fire_service_feed/ -""" +"""Support for NSW Rural Fire Service Feeds.""" from datetime import timedelta import logging from typing import Optional @@ -13,8 +8,8 @@ import voluptuous as vol from homeassistant.components.geo_location import ( PLATFORM_SCHEMA, GeolocationEvent) from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_RADIUS, CONF_SCAN_INTERVAL, - EVENT_HOMEASSISTANT_START, CONF_LATITUDE, CONF_LONGITUDE) + ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_LATITUDE, CONF_LONGITUDE, + CONF_RADIUS, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( diff --git a/homeassistant/components/geo_location/usgs_earthquakes_feed.py b/homeassistant/components/geo_location/usgs_earthquakes_feed.py index 6a7bbba4464..1d11b1971cc 100644 --- a/homeassistant/components/geo_location/usgs_earthquakes_feed.py +++ b/homeassistant/components/geo_location/usgs_earthquakes_feed.py @@ -1,9 +1,4 @@ -""" -U.S. Geological Survey Earthquake Hazards Program Feed platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/geo_location/usgs_earthquakes_feed/ -""" +"""Support for U.S. Geological Survey Earthquake Hazards Program Feeds.""" from datetime import timedelta import logging from typing import Optional @@ -13,8 +8,8 @@ import voluptuous as vol from homeassistant.components.geo_location import ( PLATFORM_SCHEMA, GeolocationEvent) from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_RADIUS, CONF_SCAN_INTERVAL, - EVENT_HOMEASSISTANT_START, CONF_LATITUDE, CONF_LONGITUDE) + ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, + CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index f265bd3492a..f27798e9e0d 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -1,19 +1,15 @@ -""" -Support for Geofency. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/geofency/ -""" +"""Support for Geofency.""" import logging -import voluptuous as vol from aiohttp import web +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER -from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME, \ - ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID, HTTP_OK, ATTR_NAME +from homeassistant.const import ( + ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_NAME, CONF_WEBHOOK_ID, HTTP_OK, + HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME) from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util import slugify @@ -27,9 +23,7 @@ CONF_MOBILE_BEACONS = 'mobile_beacons' CONFIG_SCHEMA = vol.Schema({ vol.Optional(DOMAIN): vol.Schema({ vol.Optional(CONF_MOBILE_BEACONS, default=[]): vol.All( - cv.ensure_list, - [cv.string] - ), + cv.ensure_list, [cv.string]), }), }, extra=vol.ALLOW_EXTRA) @@ -62,7 +56,7 @@ WEBHOOK_SCHEMA = vol.Schema({ vol.Required(ATTR_NAME): vol.All(cv.string, slugify), vol.Optional(ATTR_CURRENT_LATITUDE): cv.latitude, vol.Optional(ATTR_CURRENT_LONGITUDE): cv.longitude, - vol.Optional(ATTR_BEACON_ID): cv.string + vol.Optional(ATTR_BEACON_ID): cv.string, }, extra=vol.ALLOW_EXTRA) @@ -114,18 +108,11 @@ def _set_location(hass, data, location_name): device = _device_name(data) async_dispatcher_send( - hass, - TRACKER_UPDATE, - device, - (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), - location_name, - data - ) + hass, TRACKER_UPDATE, device, + (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), location_name, data) return web.Response( - text="Setting location for {}".format(device), - status=HTTP_OK - ) + text="Setting location for {}".format(device), status=HTTP_OK) async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index eea0960ec11..51201240c1c 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -6,10 +6,10 @@ https://home-assistant.io/components/device_tracker.geofency/ """ import logging -from homeassistant.components.device_tracker import DOMAIN as \ - DEVICE_TRACKER_DOMAIN -from homeassistant.components.geofency import TRACKER_UPDATE, \ - DOMAIN as GEOFENCY_DOMAIN +from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER_DOMAIN) +from homeassistant.components.geofency import ( + DOMAIN as GEOFENCY_DOMAIN, TRACKER_UPDATE) from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/goalfeed/__init__.py b/homeassistant/components/goalfeed/__init__.py index c16390302d6..6f0149f657a 100644 --- a/homeassistant/components/goalfeed/__init__.py +++ b/homeassistant/components/goalfeed/__init__.py @@ -1,9 +1,4 @@ -""" -Component for the Goalfeed service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/goalfeed/ -""" +"""Component for the Goalfeed service.""" import json import requests @@ -48,8 +43,8 @@ def setup(hass, config): 'username': username, 'password': password, 'connection_info': data} - resp = requests.post(GOALFEED_AUTH_ENDPOINT, post_data, - timeout=30).json() + resp = requests.post( + GOALFEED_AUTH_ENDPOINT, post_data, timeout=30).json() channel = pusher.subscribe('private-goals', resp['auth']) channel.bind('goal', goal_handler) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 49cb195d6c9..8fba016df57 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -1,14 +1,4 @@ -""" -Support for Google - Calendar Event Devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/google/ - -NOTE TO OTHER DEVELOPERS: IF YOU ADD MORE SCOPES TO THE OAUTH THAN JUST -CALENDAR THEN USERS WILL NEED TO DELETE THEIR TOKEN_FILE. THEY WILL LOSE THEIR -REFRESH_TOKEN PIECE WHEN RE-AUTHENTICATING TO ADD MORE API ACCESS -IT'S BEST TO JUST HAVE SEPARATE OAUTH FOR DIFFERENT PIECES OF GOOGLE -""" +"""Support for Google - Calendar Event Devices.""" import logging import os import yaml @@ -75,10 +65,10 @@ CONFIG_SCHEMA = vol.Schema({ _SINGLE_CALSEARCH_CONFIG = vol.Schema({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_DEVICE_ID): cv.string, - vol.Optional(CONF_TRACK): cv.boolean, - vol.Optional(CONF_SEARCH): cv.string, - vol.Optional(CONF_OFFSET): cv.string, vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean, + vol.Optional(CONF_OFFSET): cv.string, + vol.Optional(CONF_SEARCH): cv.string, + vol.Optional(CONF_TRACK): cv.boolean, }) DEVICE_SCHEMA = vol.Schema({ @@ -95,10 +85,7 @@ def do_authentication(hass, hass_config, config): until we have an access token. """ from oauth2client.client import ( - OAuth2WebServerFlow, - OAuth2DeviceCodeError, - FlowExchangeError - ) + OAuth2WebServerFlow, OAuth2DeviceCodeError, FlowExchangeError) from oauth2client.file import Storage oauth = OAuth2WebServerFlow( @@ -152,8 +139,8 @@ def do_authentication(hass, hass_config, config): 'been found'.format(YAML_DEVICES), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) - listener = track_time_change(hass, step2_exchange, - second=range(0, 60, dev_flow.interval)) + listener = track_time_change( + hass, step2_exchange, second=range(0, 60, dev_flow.interval)) return True diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index abb4fd28dd4..cc65c6d655d 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -1,9 +1,4 @@ -""" -Support for Google Calendar Search binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/calendar.google/ -""" +"""Support for Google Calendar Search binary sensors.""" import logging from datetime import timedelta diff --git a/homeassistant/components/google/tts.py b/homeassistant/components/google/tts.py index 0d449083f72..49a945cbbfd 100644 --- a/homeassistant/components/google/tts.py +++ b/homeassistant/components/google/tts.py @@ -1,9 +1,4 @@ -""" -Support for the google speech service. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/tts.google/ -""" +"""Support for the Google speech service.""" import asyncio import logging import re @@ -101,16 +96,16 @@ class GoogleProvider(Provider): ) if request.status != 200: - _LOGGER.error("Error %d on load url %s", + _LOGGER.error("Error %d on load URL %s", request.status, request.url) - return (None, None) + return None, None data += await request.read() except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Timeout for google speech.") - return (None, None) + _LOGGER.error("Timeout for google speech") + return None, None - return ("mp3", data) + return 'mp3', data @staticmethod def _split_message_to_parts(message): diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index c0dff15d888..f5ca9b8b146 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Actions on Google Assistant Smart Home Control. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/google_assistant/ -""" +"""Support for Actions on Google Assistant Smart Home Control.""" import asyncio import logging from typing import Dict, Any @@ -37,7 +32,7 @@ ENTITY_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_EXPOSE): cv.boolean, vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_ROOM_HINT): cv.string + vol.Optional(CONF_ROOM_HINT): cv.string, }) GOOGLE_ASSISTANT_SCHEMA = vol.Schema({ @@ -49,7 +44,7 @@ GOOGLE_ASSISTANT_SCHEMA = vol.Schema({ vol.Optional(CONF_API_KEY): cv.string, vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA}, vol.Optional(CONF_ALLOW_UNLOCK, - default=DEFAULT_ALLOW_UNLOCK): cv.boolean + default=DEFAULT_ALLOW_UNLOCK): cv.boolean, }, extra=vol.PREVENT_EXTRA) CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 7153115e3ef..af2d726328e 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1,4 +1,4 @@ -"""Implement the Smart Home traits.""" +"""Implement the Google Smart Home traits.""" import logging from homeassistant.components import ( diff --git a/homeassistant/components/google_domains/__init__.py b/homeassistant/components/google_domains/__init__.py index 32bdb79557a..f884e46cc4c 100644 --- a/homeassistant/components/google_domains/__init__.py +++ b/homeassistant/components/google_domains/__init__.py @@ -1,9 +1,4 @@ -""" -Integrate with Google Domains. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/google_domains/ -""" +"""Support for Google Domains.""" import asyncio from datetime import timedelta import logging @@ -62,8 +57,8 @@ async def async_setup(hass, config): return True -async def _update_google_domains(hass, session, domain, user, password, - timeout): +async def _update_google_domains( + hass, session, domain, user, password, timeout): """Update Google Domains.""" url = UPDATE_URL.format(user, password) diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index af8bb60f8b1..18c068ea454 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Google Cloud Pub/Sub. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/google_pubsub/ -""" +"""Support for Google Cloud Pub/Sub.""" import datetime import json import logging @@ -34,7 +29,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_PROJECT_ID): cv.string, vol.Required(CONF_TOPIC_NAME): cv.string, vol.Required(CONF_SERVICE_PRINCIPAL): cv.string, - vol.Required(CONF_FILTER): FILTER_SCHEMA + vol.Required(CONF_FILTER): FILTER_SCHEMA, }), }, extra=vol.ALLOW_EXTRA) @@ -46,8 +41,8 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): config = yaml_config[DOMAIN] project_id = config[CONF_PROJECT_ID] topic_name = config[CONF_TOPIC_NAME] - service_principal_path = os.path.join(hass.config.config_dir, - config[CONF_SERVICE_PRINCIPAL]) + service_principal_path = os.path.join( + hass.config.config_dir, config[CONF_SERVICE_PRINCIPAL]) if not os.path.isfile(service_principal_path): _LOGGER.error("Path to credentials file cannot be found") diff --git a/homeassistant/components/googlehome/__init__.py b/homeassistant/components/googlehome/__init__.py index f2d5ad09350..d6a8be58967 100644 --- a/homeassistant/components/googlehome/__init__.py +++ b/homeassistant/components/googlehome/__init__.py @@ -1,9 +1,4 @@ -""" -Support Google Home units. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/googlehome/ -""" +"""Support Google Home units.""" import logging import asyncio @@ -31,11 +26,10 @@ DEFAULT_RSSI_THRESHOLD = -70 DEVICE_CONFIG = vol.Schema({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DEVICE_TYPES, - default=DEVICE_TYPES): vol.All(cv.ensure_list, - [vol.In(DEVICE_TYPES)]), - vol.Optional(CONF_RSSI_THRESHOLD, - default=DEFAULT_RSSI_THRESHOLD): vol.Coerce(int), + vol.Optional(CONF_DEVICE_TYPES, default=DEVICE_TYPES): + vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]), + vol.Optional(CONF_RSSI_THRESHOLD, default=DEFAULT_RSSI_THRESHOLD): + vol.Coerce(int), vol.Optional(CONF_TRACK_ALARMS, default=False): cv.boolean, }) diff --git a/homeassistant/components/googlehome/device_tracker.py b/homeassistant/components/googlehome/device_tracker.py index c4b490ab316..462f5db3b9b 100644 --- a/homeassistant/components/googlehome/device_tracker.py +++ b/homeassistant/components/googlehome/device_tracker.py @@ -1,9 +1,4 @@ -""" -Support for Google Home bluetooth tacker. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.googlehome/ -""" +"""Support for Google Home Bluetooth tacker.""" import logging from datetime import timedelta @@ -13,12 +8,12 @@ from homeassistant.components.googlehome import ( from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['googlehome'] DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) -_LOGGER = logging.getLogger(__name__) - async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Google Home scanner.""" diff --git a/homeassistant/components/googlehome/sensor.py b/homeassistant/components/googlehome/sensor.py index 90b9cda80bb..7577ee0b017 100644 --- a/homeassistant/components/googlehome/sensor.py +++ b/homeassistant/components/googlehome/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Google Home alarm sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.googlehome/ -""" +"""Support for Google Home alarm sensor.""" import logging from datetime import timedelta @@ -13,7 +8,6 @@ from homeassistant.const import DEVICE_CLASS_TIMESTAMP from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util - DEPENDENCIES = ['googlehome'] SCAN_INTERVAL = timedelta(seconds=10) @@ -23,13 +17,13 @@ _LOGGER = logging.getLogger(__name__) ICON = 'mdi:alarm' SENSOR_TYPES = { - 'timer': "Timer", - 'alarm': "Alarm", + 'timer': 'Timer', + 'alarm': 'Alarm', } -async def async_setup_platform(hass, config, - async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the googlehome sensor platform.""" if discovery_info is None: _LOGGER.warning( diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 39d795dcd25..2e956bd7fc5 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -1,9 +1,4 @@ -""" -Support for GPSLogger. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/gpslogger/ -""" +"""Support for GPSLogger.""" import logging import voluptuous as vol @@ -42,16 +37,16 @@ def _id(value: str) -> str: WEBHOOK_SCHEMA = vol.Schema({ + vol.Required(ATTR_DEVICE): _id, vol.Required(ATTR_LATITUDE): cv.latitude, vol.Required(ATTR_LONGITUDE): cv.longitude, - vol.Required(ATTR_DEVICE): _id, vol.Optional(ATTR_ACCURACY, default=DEFAULT_ACCURACY): vol.Coerce(float), - vol.Optional(ATTR_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float), - vol.Optional(ATTR_SPEED): vol.Coerce(float), - vol.Optional(ATTR_DIRECTION): vol.Coerce(float), + vol.Optional(ATTR_ACTIVITY): cv.string, vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), + vol.Optional(ATTR_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float), + vol.Optional(ATTR_DIRECTION): vol.Coerce(float), vol.Optional(ATTR_PROVIDER): cv.string, - vol.Optional(ATTR_ACTIVITY): cv.string + vol.Optional(ATTR_SPEED): vol.Coerce(float), }) @@ -81,14 +76,9 @@ async def handle_webhook(hass, webhook_id, request): device = data[ATTR_DEVICE] async_dispatcher_send( - hass, - TRACKER_UPDATE, - device, + hass, TRACKER_UPDATE, device, (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), - data[ATTR_BATTERY], - data[ATTR_ACCURACY], - attrs - ) + data[ATTR_BATTERY], data[ATTR_ACCURACY], attrs) return web.Response( text='Setting location for {}'.format(device), diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index 8a312afa024..90d2dc04f89 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -1,9 +1,4 @@ -""" -Support for the GPSLogger platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.gpslogger/ -""" +"""Support for the GPSLogger device tracking.""" import logging from homeassistant.components.device_tracker import DOMAIN as \ diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index 26cd80d8da2..e3f9e359f5a 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -1,9 +1,4 @@ -""" -Component that sends data to a Graphite installation. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/graphite/ -""" +"""Support for sending data to a Graphite installation.""" import logging import queue import socket @@ -69,10 +64,8 @@ class GraphiteFeeder(threading.Thread): self._quit_object = object() self._we_started = False - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, - self.start_listen) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - self.shutdown) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.start_listen) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown) hass.bus.listen(EVENT_STATE_CHANGED, self.event_listener) _LOGGER.debug("Graphite feeding to %s:%i initialized", self._host, self._port) @@ -95,7 +88,7 @@ class GraphiteFeeder(threading.Thread): self._queue.put(event) else: _LOGGER.error( - "Graphite feeder thread has died, not queuing event!") + "Graphite feeder thread has died, not queuing event") def _send_to_graphite(self, data): """Send data to Graphite.""" diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index c1e2f285772..aedc98aac31 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -1,9 +1,4 @@ -""" -Support for monitoring a GreenEye Monitor energy monitor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/greeneye_monitor/ -""" +"""Support for monitoring a GreenEye Monitor energy monitor.""" import logging import voluptuous as vol diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index d1cd88a8438..e0315209ba1 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -1,9 +1,4 @@ -""" -Provide the functionality to group entities. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/group/ -""" +"""Provide the functionality to group entities.""" import asyncio import logging diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 8e77c6bf50b..23113a1388b 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -1,9 +1,4 @@ -""" -The Habitica API component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/habitica/ -""" +"""Support for Habitica devices.""" from collections import namedtuple import logging diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index d2f13eb30e6..fb3a5670c2b 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -1,18 +1,13 @@ -""" -The Habitica sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.habitica/ -""" - -import logging +"""Support for Habitica sensors.""" from datetime import timedelta +import logging +from homeassistant.components import habitica from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from homeassistant.components import habitica _LOGGER = logging.getLogger(__name__) + MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) @@ -36,11 +31,7 @@ class HabitipyData: """Habitica API user data cache.""" def __init__(self, api): - """ - Habitica API user data cache. - - api - HAHabitipyAsync object - """ + """Habitica API user data cache.""" self.api = api self.data = None @@ -54,12 +45,7 @@ class HabitipySensor(Entity): """A generic Habitica sensor.""" def __init__(self, name, sensor_name, updater): - """ - Init a generic Habitica sensor. - - name - Habitica platform name - sensor_name - one of the names from ALL_SENSOR_TYPES - """ + """Initialize a generic Habitica sensor.""" self._name = name self._sensor_name = sensor_name self._sensor_type = habitica.SENSORS_TYPES[sensor_name] diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 01d81cc466c..4796744c170 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -1,9 +1,4 @@ -""" -The hangouts bot component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hangouts/ -""" +"""Support for Hangouts.""" import logging import voluptuous as vol @@ -11,21 +6,18 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.hangouts.intents import HelpIntent from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import intent -from homeassistant.helpers import dispatcher +from homeassistant.helpers import dispatcher, intent import homeassistant.helpers.config_validation as cv -from .const import ( - CONF_BOT, CONF_INTENTS, CONF_REFRESH_TOKEN, DOMAIN, - EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, - MESSAGE_SCHEMA, SERVICE_SEND_MESSAGE, - SERVICE_UPDATE, CONF_SENTENCES, CONF_MATCHERS, - CONF_ERROR_SUPPRESSED_CONVERSATIONS, INTENT_SCHEMA, TARGETS_SCHEMA, - CONF_DEFAULT_CONVERSATIONS, EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, - INTENT_HELP, SERVICE_RECONNECT) - # We need an import from .config_flow, without it .config_flow is never loaded. from .config_flow import HangoutsFlowHandler # noqa: F401 +from .const import ( + CONF_BOT, CONF_DEFAULT_CONVERSATIONS, CONF_ERROR_SUPPRESSED_CONVERSATIONS, + CONF_INTENTS, CONF_MATCHERS, CONF_REFRESH_TOKEN, CONF_SENTENCES, DOMAIN, + EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, + EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, INTENT_HELP, INTENT_SCHEMA, + MESSAGE_SCHEMA, SERVICE_RECONNECT, SERVICE_SEND_MESSAGE, SERVICE_UPDATE, + TARGETS_SCHEMA) REQUIREMENTS = ['hangups==0.4.6'] @@ -39,7 +31,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_DEFAULT_CONVERSATIONS, default=[]): [TARGETS_SCHEMA], vol.Optional(CONF_ERROR_SUPPRESSED_CONVERSATIONS, default=[]): - [TARGETS_SCHEMA] + [TARGETS_SCHEMA], }) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/hangouts/config_flow.py b/homeassistant/components/hangouts/config_flow.py index 9d66338dff0..5eecc24d45e 100644 --- a/homeassistant/components/hangouts/config_flow.py +++ b/homeassistant/components/hangouts/config_flow.py @@ -5,8 +5,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import callback -from .const import CONF_2FA, CONF_REFRESH_TOKEN -from .const import DOMAIN as HANGOUTS_DOMAIN +from .const import CONF_2FA, CONF_REFRESH_TOKEN, DOMAIN as HANGOUTS_DOMAIN @callback diff --git a/homeassistant/components/hangouts/const.py b/homeassistant/components/hangouts/const.py index cf5374c317e..ca0fdf986ee 100644 --- a/homeassistant/components/hangouts/const.py +++ b/homeassistant/components/hangouts/const.py @@ -3,8 +3,8 @@ import logging import voluptuous as vol -from homeassistant.components.notify \ - import ATTR_MESSAGE, ATTR_TARGET, ATTR_DATA +from homeassistant.components.notify import ( + ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger('homeassistant.components.hangouts') diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 748079452d8..fe72c50de77 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -1,17 +1,19 @@ """The Hangouts Bot.""" +import asyncio import io import logging -import asyncio + import aiohttp -from homeassistant.helpers.aiohttp_client import async_get_clientsession + from homeassistant.helpers import dispatcher, intent +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( - ATTR_MESSAGE, ATTR_TARGET, ATTR_DATA, CONF_CONVERSATIONS, DOMAIN, + ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, CONF_CONVERSATION_ID, + CONF_CONVERSATION_NAME, CONF_CONVERSATIONS, CONF_MATCHERS, DOMAIN, EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, - EVENT_HANGOUTS_DISCONNECTED, EVENT_HANGOUTS_MESSAGE_RECEIVED, - CONF_MATCHERS, CONF_CONVERSATION_ID, - CONF_CONVERSATION_NAME, EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, INTENT_HELP) + EVENT_HANGOUTS_CONVERSATIONS_RESOLVED, EVENT_HANGOUTS_DISCONNECTED, + EVENT_HANGOUTS_MESSAGE_RECEIVED, INTENT_HELP) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hangouts/intents.py b/homeassistant/components/hangouts/intents.py index be52f059139..3887a644700 100644 --- a/homeassistant/components/hangouts/intents.py +++ b/homeassistant/components/hangouts/intents.py @@ -1,8 +1,8 @@ -"""Intents for the hangouts component.""" +"""Intents for the Hangouts component.""" from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv -from .const import INTENT_HELP, DOMAIN, CONF_BOT +from .const import CONF_BOT, DOMAIN, INTENT_HELP class HelpIntent(intent.IntentHandler): diff --git a/homeassistant/components/hangouts/notify.py b/homeassistant/components/hangouts/notify.py index 7261663b99f..c3b5450be05 100644 --- a/homeassistant/components/hangouts/notify.py +++ b/homeassistant/components/hangouts/notify.py @@ -1,20 +1,13 @@ -""" -Hangouts notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.hangouts/ -""" +"""Support for Hangouts notifications.""" import logging import voluptuous as vol -from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA, - BaseNotificationService, - ATTR_MESSAGE, ATTR_DATA) - -from homeassistant.components.hangouts.const \ - import (DOMAIN, SERVICE_SEND_MESSAGE, TARGETS_SCHEMA, - CONF_DEFAULT_CONVERSATIONS) +from homeassistant.components.hangouts.const import ( + CONF_DEFAULT_CONVERSATIONS, DOMAIN, SERVICE_SEND_MESSAGE, TARGETS_SCHEMA) +from homeassistant.components.notify import ( + ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, PLATFORM_SCHEMA, + BaseNotificationService) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index 25a33929c1a..12ccc78077e 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -1,5 +1 @@ -"""The harmony component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/harmony/ -""" +"""Support for Harmony devices.""" diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index a5e4f5a8528..612ea735aaa 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -1,9 +1,4 @@ -""" -Support for Harmony Hub devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/remote.harmony/ -""" +"""Support for Harmony Hub devices.""" import asyncio import json import logging @@ -51,12 +46,12 @@ HARMONY_SYNC_SCHEMA = vol.Schema({ HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema({ vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_CHANNEL): cv.positive_int + vol.Required(ATTR_CHANNEL): cv.positive_int, }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Harmony platform.""" activity = None diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 3c058281b0a..e070c889f31 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -1,9 +1,4 @@ -""" -Exposes regular REST commands as services. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hassio/ -""" +"""Support for Hass.io.""" from datetime import timedelta import logging import os @@ -14,16 +9,15 @@ from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import SERVICE_CHECK_CONFIG from homeassistant.const import ( ATTR_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP) -from homeassistant.core import DOMAIN as HASS_DOMAIN -from homeassistant.core import callback +from homeassistant.core import DOMAIN as HASS_DOMAIN, callback +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow -from homeassistant.exceptions import HomeAssistantError from .auth import async_setup_auth -from .handler import HassIO, HassioAPIError from .discovery import async_setup_discovery +from .handler import HassIO, HassioAPIError from .http import HassIOView _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 4be3ba9956c..b104d53aff9 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -1,20 +1,20 @@ """Implement the auth feature from Hass.io for Add-ons.""" -import logging from ipaddress import ip_address +import logging import os from aiohttp import web from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.exceptions import HomeAssistantError from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv -from .const import ATTR_USERNAME, ATTR_PASSWORD, ATTR_ADDON +from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index a5f62b9e1a1..804247d2407 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -1,18 +1,18 @@ -"""Implement the serivces discovery feature from Hass.io for Add-ons.""" +"""Implement the services discovery feature from Hass.io for Add-ons.""" import asyncio import logging from aiohttp import web from aiohttp.web_exceptions import HTTPServiceUnavailable -from homeassistant.core import callback, CoreState -from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.components.http import HomeAssistantView +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import CoreState, callback -from .handler import HassioAPIError from .const import ( - ATTR_DISCOVERY, ATTR_ADDON, ATTR_NAME, ATTR_SERVICE, ATTR_CONFIG, + ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_NAME, ATTR_SERVICE, ATTR_UUID) +from .handler import HassioAPIError _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index c33125d840e..640ed29e578 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -1,9 +1,4 @@ -""" -Exposes regular REST commands as services. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hassio/ -""" +"""Handler for Hass.io.""" import asyncio import logging import os diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 6b8004f7664..01ded9ca11d 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -1,23 +1,18 @@ -""" -Exposes regular REST commands as services. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hassio/ -""" +"""HTTP Support for Hass.io.""" import asyncio import logging import os import re -import async_timeout import aiohttp from aiohttp import web from aiohttp.hdrs import CONTENT_TYPE from aiohttp.web_exceptions import HTTPBadGateway +import async_timeout from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView -from .const import X_HASSIO, X_HASS_USER_ID, X_HASS_IS_ADMIN +from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID, X_HASSIO _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 5fb2e19edcf..8eb13c5ab21 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -1,26 +1,20 @@ -""" -HDMI CEC component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/hdmi_cec/ -""" -import logging -import multiprocessing +"""Support for HDMI CEC.""" from collections import defaultdict from functools import reduce +import logging +import multiprocessing import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER from homeassistant.components.switch import DOMAIN as SWITCH -from homeassistant.const import (EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, STATE_ON, - STATE_OFF, CONF_DEVICES, CONF_PLATFORM, - STATE_PLAYING, STATE_IDLE, - STATE_PAUSED, CONF_HOST) +from homeassistant.const import ( + CONF_DEVICES, CONF_HOST, CONF_PLATFORM, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, + STATE_PLAYING) from homeassistant.core import HomeAssistant +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity REQUIREMENTS = ['pyCEC==0.4.13'] @@ -43,7 +37,7 @@ ICONS_BY_TYPE = { 1: ICON_RECORDER, 3: ICON_TUNER, 4: ICON_PLAYER, - 5: ICON_AUDIO + 5: ICON_AUDIO, } CEC_DEVICES = defaultdict(list) @@ -87,7 +81,7 @@ SERVICE_SEND_COMMAND_SCHEMA = vol.Schema({ vol.Optional(ATTR_SRC): _VOL_HEX, vol.Optional(ATTR_DST): _VOL_HEX, vol.Optional(ATTR_ATT): _VOL_HEX, - vol.Optional(ATTR_RAW): vol.Coerce(str) + vol.Optional(ATTR_RAW): vol.Coerce(str), }, extra=vol.PREVENT_EXTRA) SERVICE_VOLUME = 'volume' diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index 6e691cad94f..553983a1f03 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -1,9 +1,4 @@ -""" -Support for HDMI CEC devices as media players. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/hdmi_cec/ -""" +"""Support for HDMI CEC devices as media players.""" import logging from homeassistant.components.hdmi_cec import ATTR_NEW, CecDevice @@ -25,7 +20,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return HDMI devices as +switches.""" if ATTR_NEW in discovery_info: - _LOGGER.info("Setting up HDMI devices %s", discovery_info[ATTR_NEW]) + _LOGGER.debug("Setting up HDMI devices %s", discovery_info[ATTR_NEW]) entities = [] for device in discovery_info[ATTR_NEW]: hdmi_device = hass.data.get(device) diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py index 1016e91d8d2..ff423890ba5 100644 --- a/homeassistant/components/hdmi_cec/switch.py +++ b/homeassistant/components/hdmi_cec/switch.py @@ -1,14 +1,9 @@ -""" -Support for HDMI CEC devices as switches. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/hdmi_cec/ -""" +"""Support for HDMI CEC devices as switches.""" import logging -from homeassistant.components.hdmi_cec import CecDevice, ATTR_NEW -from homeassistant.components.switch import SwitchDevice, DOMAIN -from homeassistant.const import STATE_OFF, STATE_STANDBY, STATE_ON +from homeassistant.components.hdmi_cec import ATTR_NEW, CecDevice +from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY DEPENDENCIES = ['hdmi_cec'] diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 1773a55b3f1..fb037e8909d 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -1,9 +1,4 @@ -""" -Provide pre-made queries on top of the recorder component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/history/ -""" +"""Provide pre-made queries on top of the recorder component.""" from collections import defaultdict from datetime import timedelta from itertools import groupby diff --git a/homeassistant/components/history_graph/__init__.py b/homeassistant/components/history_graph/__init__.py index 7d9db379705..893f3514d77 100644 --- a/homeassistant/components/history_graph/__init__.py +++ b/homeassistant/components/history_graph/__init__.py @@ -1,9 +1,4 @@ -""" -Support to graphs card in the UI. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/history_graph/ -""" +"""Support to graphs card in the UI.""" import logging import voluptuous as vol @@ -34,7 +29,7 @@ GRAPH_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({ - DOMAIN: cv.schema_with_slug_keys(GRAPH_SCHEMA) + DOMAIN: cv.schema_with_slug_keys(GRAPH_SCHEMA), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 09319849933..934c44028ac 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -1,20 +1,17 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/hive/ -""" +"""Support for the Hive devices.""" import logging + import voluptuous as vol -from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL, - CONF_USERNAME) +from homeassistant.const import ( + CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform REQUIREMENTS = ['pyhiveapi==0.2.17'] _LOGGER = logging.getLogger(__name__) + DOMAIN = 'hive' DATA_HIVE = 'data_hive' DEVICETYPES = { @@ -23,7 +20,7 @@ DEVICETYPES = { 'light': 'device_list_light', 'switch': 'device_list_plug', 'sensor': 'device_list_sensor', - } +} CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -59,9 +56,8 @@ def setup(hass, config): password = config[DOMAIN][CONF_PASSWORD] update_interval = config[DOMAIN][CONF_SCAN_INTERVAL] - devicelist = session.core.initialise_api(username, - password, - update_interval) + devicelist = session.core.initialise_api( + username, password, update_interval) if devicelist is None: _LOGGER.error("Hive API initialization failed") diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index e114e67f90f..dee27c5c710 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,16 +1,13 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.hive/ -""" +"""Support for the Hive binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.hive import DATA_HIVE, DOMAIN DEPENDENCIES = ['hive'] -DEVICETYPE_DEVICE_CLASS = {'motionsensor': 'motion', - 'contactsensor': 'opening'} +DEVICETYPE_DEVICE_CLASS = { + 'motionsensor': 'motion', + 'contactsensor': 'opening', +} def setup_platform(hass, config, add_entities, discovery_info=None): @@ -76,8 +73,8 @@ class HiveBinarySensorEntity(BinarySensorDevice): @property def is_on(self): """Return true if the binary sensor is on.""" - return self.session.sensor.get_state(self.node_id, - self.node_device_type) + return self.session.sensor.get_state( + self.node_id, self.node_device_type) def update(self): """Update all Node data from Hive.""" diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 87d426d6f05..2ecc195061a 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,20 +1,25 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.hive/ -""" +"""Support for the Hive climate devices.""" from homeassistant.components.climate import ( - ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON, - SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON, SUPPORT_AUX_HEAT, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) from homeassistant.components.hive import DATA_HIVE, DOMAIN +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS DEPENDENCIES = ['hive'] -HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT, - 'ON': STATE_ON, 'OFF': STATE_OFF} -HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL', - STATE_ON: 'ON', STATE_OFF: 'OFF'} + +HIVE_TO_HASS_STATE = { + 'SCHEDULE': STATE_AUTO, + 'MANUAL': STATE_HEAT, + 'ON': STATE_ON, + 'OFF': STATE_OFF, +} + +HASS_TO_HIVE_STATE = { + STATE_AUTO: 'SCHEDULE', + STATE_HEAT: 'MANUAL', + STATE_ON: 'ON', + STATE_OFF: 'OFF', +} SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | @@ -42,8 +47,8 @@ class HiveClimateEntity(ClimateDevice): self.thermostat_node_id = hivedevice["Thermostat_NodeID"] self.session = hivesession self.attributes = {} - self.data_updatesource = '{}.{}'.format(self.device_type, - self.node_id) + self.data_updatesource = '{}.{}'.format( + self.device_type, self.node_id) self._unique_id = '{}-{}'.format(self.node_id, self.device_type) if self.device_type == "Heating": diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index c2bb95f40da..2bec60f0ee4 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -1,15 +1,8 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.hive/ -""" +"""Support for the Hive lights.""" from homeassistant.components.hive import DATA_HIVE, DOMAIN -from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, - SUPPORT_COLOR, Light) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) import homeassistant.util.color as color_util DEPENDENCIES = ['hive'] @@ -35,8 +28,8 @@ class HiveDeviceLight(Light): self.light_device_type = hivedevice["Hive_Light_DeviceType"] self.session = hivesession self.attributes = {} - self.data_updatesource = '{}.{}'.format(self.device_type, - self.node_id) + self.data_updatesource = '{}.{}'.format( + self.device_type, self.node_id) self._unique_id = '{}-{}'.format(self.node_id, self.device_type) self.session.entities.append(self) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index e989074fb4b..142c8c7ee94 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -1,19 +1,19 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.hive/ -""" -from homeassistant.const import TEMP_CELSIUS +"""Support for the Hive sensors.""" from homeassistant.components.hive import DATA_HIVE, DOMAIN +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity DEPENDENCIES = ['hive'] -FRIENDLY_NAMES = {'Hub_OnlineStatus': 'Hive Hub Status', - 'Hive_OutsideTemperature': 'Outside Temperature'} -DEVICETYPE_ICONS = {'Hub_OnlineStatus': 'mdi:switch', - 'Hive_OutsideTemperature': 'mdi:thermometer'} +FRIENDLY_NAMES = { + 'Hub_OnlineStatus': 'Hive Hub Status', + 'Hive_OutsideTemperature': 'Outside Temperature', +} + +DEVICETYPE_ICONS = { + 'Hub_OnlineStatus': 'mdi:switch', + 'Hive_OutsideTemperature': 'mdi:thermometer', +} def setup_platform(hass, config, add_entities, discovery_info=None): @@ -36,8 +36,8 @@ class HiveSensorEntity(Entity): self.device_type = hivedevice["HA_DeviceType"] self.node_device_type = hivedevice["Hive_DeviceType"] self.session = hivesession - self.data_updatesource = '{}.{}'.format(self.device_type, - self.node_id) + self.data_updatesource = '{}.{}'.format( + self.device_type, self.node_id) self._unique_id = '{}-{}'.format(self.node_id, self.device_type) self.session.entities.append(self) diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index a50323f0a4e..c897e37f34b 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -1,11 +1,6 @@ -""" -Support for the Hive devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.hive/ -""" -from homeassistant.components.switch import SwitchDevice +"""Support for the Hive switches.""" from homeassistant.components.hive import DATA_HIVE, DOMAIN +from homeassistant.components.switch import SwitchDevice DEPENDENCIES = ['hive'] @@ -29,8 +24,8 @@ class HiveDevicePlug(SwitchDevice): self.device_type = hivedevice["HA_DeviceType"] self.session = hivesession self.attributes = {} - self.data_updatesource = '{}.{}'.format(self.device_type, - self.node_id) + self.data_updatesource = '{}.{}'.format( + self.device_type, self.node_id) self._unique_id = '{}-{}'.format(self.node_id, self.device_type) self.session.entities.append(self) diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index cfbb8ac010c..aab3f79b8b2 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -1,9 +1,4 @@ -""" -Support for HLK-SW16 relay switch. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/hlk_sw16/ -""" +"""Support for HLK-SW16 relay switches.""" import logging import voluptuous as vol diff --git a/homeassistant/components/hlk_sw16/switch.py b/homeassistant/components/hlk_sw16/switch.py index d76528c56f0..b1bfc5ce23d 100644 --- a/homeassistant/components/hlk_sw16/switch.py +++ b/homeassistant/components/hlk_sw16/switch.py @@ -1,9 +1,4 @@ -""" -Support for HLK-SW16 switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.hlk_sw16/ -""" +"""Support for HLK-SW16 switches.""" import logging from homeassistant.components.hlk_sw16 import ( @@ -31,8 +26,8 @@ def devices_from_config(hass, domain_config): return devices -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the HLK-SW16 platform.""" async_add_entities(devices_from_config(hass, discovery_info)) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c34b527252f..01979f03b9a 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -1,8 +1,4 @@ -"""Support for Apple HomeKit. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/homekit/ -""" +"""Support for Apple HomeKit.""" import ipaddress import logging from zlib import adler32 @@ -14,19 +10,19 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, CONF_TYPE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - TEMP_CELSIUS, TEMP_FAHRENHEIT) + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, + TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry + from .const import ( BRIDGE_NAME, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, CONF_FILTER, CONF_SAFE_MODE, DEFAULT_AUTO_START, DEFAULT_PORT, - DEFAULT_SAFE_MODE, DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, - SERVICE_HOMEKIT_START, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, - TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) + DEFAULT_SAFE_MODE, DEVICE_CLASS_CO, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, + DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, TYPE_FAUCET, TYPE_OUTLET, + TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 5baed0294b8..4adeda2465a 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -9,10 +9,9 @@ from pyhap.accessory_driver import AccessoryDriver from pyhap.const import CATEGORY_OTHER from homeassistant.const import ( - __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, - ATTR_SERVICE) -from homeassistant.core import callback as ha_callback -from homeassistant.core import split_entity_id + ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_SERVICE, + __version__) +from homeassistant.core import callback as ha_callback, split_entity_id from homeassistant.helpers.event import ( async_track_state_change, track_point_in_utc_time) from homeassistant.util import dt as dt_util @@ -22,8 +21,7 @@ from .const import ( CHAR_BATTERY_LEVEL, CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, DEBOUNCE_TIMEOUT, EVENT_HOMEKIT_CHANGED, MANUFACTURER, SERV_BATTERY_SERVICE) -from .util import ( - convert_to_float, show_setup_message, dismiss_setup_message) +from .util import convert_to_float, dismiss_setup_message, show_setup_message _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index b3beb11c8b6..5273480b6ce 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -11,11 +11,11 @@ from homeassistant.const import ( STATE_CLOSED, STATE_OPEN) from . import TYPES -from .accessories import debounce, HomeAccessory +from .accessories import HomeAccessory, debounce from .const import ( CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, CHAR_POSITION_STATE, - CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION, - SERV_GARAGE_DOOR_OPENER, SERV_WINDOW_COVERING) + CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION, SERV_GARAGE_DOOR_OPENER, + SERV_WINDOW_COVERING) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index dcc93b7cf9e..d2777a296dc 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -13,7 +13,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON) from . import TYPES -from .accessories import debounce, HomeAccessory +from .accessories import HomeAccessory, debounce from .const import ( CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_ROTATION_SPEED, CHAR_SWING_MODE, SERV_FANV2) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index a9007ace35b..f549958f755 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -5,17 +5,17 @@ from pyhap.const import CATEGORY_LIGHTBULB from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, - ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, DOMAIN, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP) + ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, DOMAIN, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON, - SERVICE_TURN_OFF, STATE_OFF, STATE_ON) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, STATE_ON) from . import TYPES -from .accessories import debounce, HomeAccessory +from .accessories import HomeAccessory, debounce from .const import ( CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, CHAR_ON, - CHAR_SATURATION, SERV_LIGHTBULB, PROP_MAX_VALUE, PROP_MIN_VALUE) + CHAR_SATURATION, PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_LIGHTBULB) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 22c47d59c62..4ed1cebd207 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -13,13 +13,19 @@ from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK _LOGGER = logging.getLogger(__name__) -HASS_TO_HOMEKIT = {STATE_UNLOCKED: 0, - STATE_LOCKED: 1, - # value 2 is Jammed which hass doesn't have a state for - STATE_UNKNOWN: 3} +HASS_TO_HOMEKIT = { + STATE_UNLOCKED: 0, + STATE_LOCKED: 1, + # Value 2 is Jammed which hass doesn't have a state for + STATE_UNKNOWN: 3, +} + HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} -STATE_TO_SERVICE = {STATE_LOCKED: 'lock', - STATE_UNLOCKED: 'unlock'} + +STATE_TO_SERVICE = { + STATE_LOCKED: 'lock', + STATE_UNLOCKED: 'unlock', +} @TYPES.register('Lock') @@ -45,7 +51,7 @@ class Lock(HomeAccessory): def set_state(self, value): """Set lock state to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set state to %d', self.entity_id, value) + _LOGGER.debug("%s: Set state to %d", self.entity_id, value) self._flag_state = True hass_value = HOMEKIT_TO_HASS.get(value) @@ -62,7 +68,7 @@ class Lock(HomeAccessory): if hass_state in HASS_TO_HOMEKIT: current_lock_state = HASS_TO_HOMEKIT[hass_state] self.char_current_state.set_value(current_lock_state) - _LOGGER.debug('%s: Updated current state to %s (%d)', + _LOGGER.debug("%s: Updated current state to %s (%d)", self.entity_id, hass_state, current_lock_state) # LockTargetState only supports locked and unlocked diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 09088871fd2..f8f4ef96992 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -3,12 +3,12 @@ import logging from pyhap.const import CATEGORY_SWITCH +from homeassistant.components.media_player import ( + ATTR_MEDIA_VOLUME_MUTED, DOMAIN) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, STATE_OFF, STATE_PLAYING, STATE_UNKNOWN) -from homeassistant.components.media_player import ( - ATTR_MEDIA_VOLUME_MUTED, DOMAIN) from . import TYPES from .accessories import HomeAccessory @@ -18,10 +18,12 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -MODE_FRIENDLY_NAME = {FEATURE_ON_OFF: 'Power', - FEATURE_PLAY_PAUSE: 'Play/Pause', - FEATURE_PLAY_STOP: 'Play/Stop', - FEATURE_TOGGLE_MUTE: 'Mute'} +MODE_FRIENDLY_NAME = { + FEATURE_ON_OFF: 'Power', + FEATURE_PLAY_PAUSE: 'Play/Pause', + FEATURE_PLAY_STOP: 'Play/Stop', + FEATURE_TOGGLE_MUTE: 'Mute', +} @TYPES.register('MediaPlayer') diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index e210217df2f..10befb4af61 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -5,10 +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, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, + ATTR_CODE, ATTR_ENTITY_ID, 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) + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED) from . import TYPES from .accessories import HomeAccessory @@ -18,17 +18,22 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -HASS_TO_HOMEKIT = {STATE_ALARM_ARMED_HOME: 0, - STATE_ALARM_ARMED_AWAY: 1, - STATE_ALARM_ARMED_NIGHT: 2, - STATE_ALARM_DISARMED: 3, - STATE_ALARM_TRIGGERED: 4} +HASS_TO_HOMEKIT = { + STATE_ALARM_ARMED_HOME: 0, + STATE_ALARM_ARMED_AWAY: 1, + STATE_ALARM_ARMED_NIGHT: 2, + 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_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} + STATE_ALARM_DISARMED: SERVICE_ALARM_DISARM, +} @TYPES.register('SecuritySystem') diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 09da361ddb8..0d7dd94d014 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -4,7 +4,7 @@ import logging from pyhap.const import CATEGORY_SENSOR from homeassistant.const import ( - ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_HOME, + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_HOME, STATE_ON, TEMP_CELSIUS) from . import TYPES @@ -26,7 +26,7 @@ from .const import ( SERV_LIGHT_SENSOR, SERV_MOTION_SENSOR, SERV_OCCUPANCY_SENSOR, SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR, THRESHOLD_CO, THRESHOLD_CO2) from .util import ( - convert_to_float, temperature_to_homekit, density_to_air_quality) + convert_to_float, density_to_air_quality, temperature_to_homekit) _LOGGER = logging.getLogger(__name__) @@ -42,7 +42,8 @@ BINARY_SENSOR_SERVICE_MAP = { DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED), DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED), - DEVICE_CLASS_WINDOW: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE)} + DEVICE_CLASS_WINDOW: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), +} @TYPES.register('TemperatureSensor') diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index b41e1a01543..7629e33a4d7 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -2,13 +2,13 @@ import logging from pyhap.const import ( - CATEGORY_FAUCET, CATEGORY_OUTLET, CATEGORY_SHOWER_HEAD, - CATEGORY_SPRINKLER, CATEGORY_SWITCH) + CATEGORY_FAUCET, CATEGORY_OUTLET, CATEGORY_SHOWER_HEAD, CATEGORY_SPRINKLER, + CATEGORY_SWITCH) from homeassistant.components.script import ATTR_CAN_CANCEL from homeassistant.components.switch import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_TYPE, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) + ATTR_ENTITY_ID, CONF_TYPE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) from homeassistant.core import split_entity_id from homeassistant.helpers.event import call_later diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index f78a05b1a45..4a9ccdee009 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -5,30 +5,30 @@ from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, - ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN as DOMAIN_CLIMATE, + ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, + DOMAIN as DOMAIN_CLIMATE, SERVICE_SET_OPERATION_MODE as SERVICE_SET_OPERATION_MODE_THERMOSTAT, - SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, - STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) + SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, STATE_AUTO, + STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.water_heater import ( DOMAIN as DOMAIN_WATER_HEATER, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_WATER_HEATER) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, - SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, TEMP_CELSIUS, + TEMP_FAHRENHEIT) from . import TYPES -from .accessories import debounce, HomeAccessory +from .accessories import HomeAccessory, debounce from .const import ( CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, - CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, - CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE, - CHAR_TEMP_DISPLAY_UNITS, - DEFAULT_MAX_TEMP_WATER_HEATER, DEFAULT_MIN_TEMP_WATER_HEATER, - PROP_MAX_VALUE, PROP_MIN_STEP, PROP_MIN_VALUE, SERV_THERMOSTAT) + CHAR_CURRENT_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE, + CHAR_TARGET_HEATING_COOLING, CHAR_TARGET_TEMPERATURE, + CHAR_TEMP_DISPLAY_UNITS, DEFAULT_MAX_TEMP_WATER_HEATER, + DEFAULT_MIN_TEMP_WATER_HEATER, PROP_MAX_VALUE, PROP_MIN_STEP, + PROP_MIN_VALUE, SERV_THERMOSTAT) from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index f1327f8b527..2ba5819a202 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -1,5 +1,5 @@ """Collection of useful functions for the HomeKit component.""" -from collections import namedtuple, OrderedDict +from collections import OrderedDict, namedtuple import logging import voluptuous as vol diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 77d0825ef0b..ffeb9d625be 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Homekit device discovery. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homekit_controller/ -""" +"""Support for Homekit device discovery.""" import json import logging import os @@ -35,7 +30,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { HOMEKIT_IGNORE = [ 'BSB002', 'Home Assistant Bridge', - 'TRADFRI gateway' + 'TRADFRI gateway', ] KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN) diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index 3a2e5170453..5d366b6e27b 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -1,18 +1,12 @@ -""" -Support for Homekit Alarm Control Panel. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.homekit_controller/ -""" +"""Support for Homekit Alarm Control Panel.""" import logging -from homeassistant.components.homekit_controller import (HomeKitEntity, - KNOWN_ACCESSORIES) from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.homekit_controller import ( + KNOWN_ACCESSORIES, HomeKitEntity) from homeassistant.const import ( - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED) -from homeassistant.const import ATTR_BATTERY_LEVEL + ATTR_BATTERY_LEVEL, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) DEPENDENCIES = ['homekit_controller'] @@ -25,7 +19,7 @@ CURRENT_STATE_MAP = { 1: STATE_ALARM_ARMED_AWAY, 2: STATE_ALARM_ARMED_NIGHT, 3: STATE_ALARM_DISARMED, - 4: STATE_ALARM_TRIGGERED + 4: STATE_ALARM_TRIGGERED, } TARGET_STATE_MAP = { diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 531297dc911..5d83ce6d984 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -1,14 +1,9 @@ -""" -Support for Homekit motion sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.homekit_controller/ -""" +"""Support for Homekit motion sensors.""" import logging -from homeassistant.components.homekit_controller import (HomeKitEntity, - KNOWN_ACCESSORIES) from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.homekit_controller import ( + KNOWN_ACCESSORIES, HomeKitEntity) DEPENDENCIES = ['homekit_controller'] diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 15378e2b046..5531b266d21 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -1,17 +1,12 @@ -""" -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/ -""" +"""Support for Homekit climate devices.""" 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 + STATE_COOL, STATE_HEAT, STATE_IDLE, SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, ClimateDevice) +from homeassistant.components.homekit_controller import ( + KNOWN_ACCESSORIES, HomeKitEntity) +from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS DEPENDENCIES = ['homekit_controller'] diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index c8f087254bb..3951cf577d4 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -1,17 +1,12 @@ -""" -Support for Homekit Cover. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.homekit_controller/ -""" +"""Support for Homekit covers.""" import logging -from homeassistant.components.homekit_controller import (HomeKitEntity, - KNOWN_ACCESSORIES) from homeassistant.components.cover import ( - CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, - SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_SET_TILT_POSITION, - ATTR_POSITION, ATTR_TILT_POSITION) + ATTR_POSITION, ATTR_TILT_POSITION, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, CoverDevice) +from homeassistant.components.homekit_controller import ( + KNOWN_ACCESSORIES, HomeKitEntity) from homeassistant.const import ( STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING) diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 74ef8948f45..f39e793c184 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -1,15 +1,10 @@ -""" -Support for Homekit lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.homekit_controller/ -""" +"""Support for Homekit lights.""" import logging from homeassistant.components.homekit_controller import ( - HomeKitEntity, KNOWN_ACCESSORIES) + KNOWN_ACCESSORIES, HomeKitEntity) from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) DEPENDENCIES = ['homekit_controller'] diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index e27ed444528..635d457198a 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -1,17 +1,11 @@ -""" -Support for HomeKit Controller locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.homekit_controller/ -""" - +"""Support for HomeKit Controller locks.""" import logging -from homeassistant.components.homekit_controller import (HomeKitEntity, - KNOWN_ACCESSORIES) +from homeassistant.components.homekit_controller import ( + KNOWN_ACCESSORIES, HomeKitEntity) from homeassistant.components.lock import LockDevice -from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED, - ATTR_BATTERY_LEVEL) +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED) DEPENDENCIES = ['homekit_controller'] @@ -28,7 +22,7 @@ CURRENT_STATE_MAP = { TARGET_STATE_MAP = { STATE_UNLOCKED: 0, - STATE_LOCKED: 1 + STATE_LOCKED: 1, } @@ -37,8 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']] - add_entities([HomeKitLock(accessory, discovery_info)], - True) + add_entities([HomeKitLock(accessory, discovery_info)], True) class HomeKitLock(HomeKitEntity, LockDevice): diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index ba4a04022f0..daa4ede6898 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -1,13 +1,8 @@ -""" -Support for Homekit switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.homekit_controller/ -""" +"""Support for Homekit switches.""" import logging -from homeassistant.components.homekit_controller import (HomeKitEntity, - KNOWN_ACCESSORIES) +from homeassistant.components.homekit_controller import ( + KNOWN_ACCESSORIES, HomeKitEntity) from homeassistant.components.switch import SwitchDevice DEPENDENCIES = ['homekit_controller'] diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 3439a23adb3..2918f8e03f9 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -1,9 +1,4 @@ -""" -Support for HomeMatic devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homematic/ -""" +"""Support for HomeMatic devices.""" from datetime import timedelta from functools import partial import logging @@ -13,7 +8,7 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, CONF_HOST, CONF_HOSTS, CONF_PASSWORD, - CONF_PLATFORM, CONF_USERNAME, CONF_SSL, CONF_VERIFY_SSL, + CONF_PLATFORM, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index 9cfe4bbd6a7..1704411c9cc 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for HomeMatic binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.homematic/ -""" +"""Support for HomeMatic binary sensors.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 5233501ec30..4fc4b190808 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -1,9 +1,4 @@ -""" -Support for Homematic thermostats. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.homematic/ -""" +"""Support for Homematic thermostats.""" import logging from homeassistant.components.climate import ( diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index 93574321203..79a1afe9a0e 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -1,9 +1,4 @@ -""" -The HomeMatic cover platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.homematic/ -""" +"""Support for HomeMatic covers.""" import logging from homeassistant.components.cover import ( diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index de11c96f8b7..21b875742c4 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -1,15 +1,10 @@ -""" -Support for Homematic lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.homematic/ -""" +"""Support for Homematic lights.""" import logging from homeassistant.components.homematic import ATTR_DISCOVER_DEVICES, HMDevice from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_COLOR, - ATTR_EFFECT, SUPPORT_EFFECT, Light) + ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_EFFECT, Light) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homematic/lock.py b/homeassistant/components/homematic/lock.py index 9d9f2a28b4f..5d857617fde 100644 --- a/homeassistant/components/homematic/lock.py +++ b/homeassistant/components/homematic/lock.py @@ -1,9 +1,4 @@ -""" -Support for Homematic lock. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.homematic/ -""" +"""Support for Homematic locks.""" import logging from homeassistant.components.homematic import ATTR_DISCOVER_DEVICES, HMDevice diff --git a/homeassistant/components/homematic/notify.py b/homeassistant/components/homematic/notify.py index 2897123c690..e6ef1a60e28 100644 --- a/homeassistant/components/homematic/notify.py +++ b/homeassistant/components/homematic/notify.py @@ -8,12 +8,12 @@ import logging import voluptuous as vol -from homeassistant.components.notify import ( - BaseNotificationService, PLATFORM_SCHEMA, ATTR_DATA) -import homeassistant.helpers.config_validation as cv from homeassistant.components.homematic import ( - DOMAIN, SERVICE_SET_DEVICE_VALUE, ATTR_ADDRESS, ATTR_CHANNEL, ATTR_PARAM, - ATTR_VALUE, ATTR_INTERFACE) + ATTR_ADDRESS, ATTR_CHANNEL, ATTR_INTERFACE, ATTR_PARAM, ATTR_VALUE, DOMAIN, + SERVICE_SET_DEVICE_VALUE) +from homeassistant.components.notify import ( + ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) +import homeassistant.helpers.config_validation as cv import homeassistant.helpers.template as template_helper _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 84cf19652a1..c4d97dca3fe 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -1,9 +1,4 @@ -""" -The HomeMatic sensor platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.homematic/ -""" +"""Support for HomeMatic sensors.""" import logging from homeassistant.components.homematic import ATTR_DISCOVER_DEVICES, HMDevice @@ -14,26 +9,14 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['homematic'] HM_STATE_HA_CAST = { - 'RotaryHandleSensor': {0: 'closed', - 1: 'tilted', - 2: 'open'}, - 'RotaryHandleSensorIP': {0: 'closed', - 1: 'tilted', - 2: 'open'}, - 'WaterSensor': {0: 'dry', - 1: 'wet', - 2: 'water'}, - 'CO2Sensor': {0: 'normal', - 1: 'added', - 2: 'strong'}, - 'IPSmoke': {0: 'off', - 1: 'primary', - 2: 'intrusion', - 3: 'secondary'}, - 'RFSiren': {0: 'disarmed', - 1: 'extsens_armed', - 2: 'allsens_armed', - 3: 'alarm_blocked'}, + 'RotaryHandleSensor': {0: 'closed', 1: 'tilted', 2: 'open'}, + 'RotaryHandleSensorIP': {0: 'closed', 1: 'tilted', 2: 'open'}, + 'WaterSensor': {0: 'dry', 1: 'wet', 2: 'water'}, + 'CO2Sensor': {0: 'normal', 1: 'added', 2: 'strong'}, + 'IPSmoke': {0: 'off', 1: 'primary', 2: 'intrusion', 3: 'secondary'}, + 'RFSiren': { + 0: 'disarmed', 1: 'extsens_armed', 2: 'allsens_armed', + 3: 'alarm_blocked'}, } HM_UNIT_HA_CAST = { diff --git a/homeassistant/components/homematic/switch.py b/homeassistant/components/homematic/switch.py index b5921819ea4..cfcd26891e0 100644 --- a/homeassistant/components/homematic/switch.py +++ b/homeassistant/components/homematic/switch.py @@ -1,9 +1,4 @@ -""" -Support for HomeMatic switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.homematic/ -""" +"""Support for HomeMatic switches.""" import logging from homeassistant.components.homematic import ATTR_DISCOVER_DEVICES, HMDevice diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index f048a50d1d0..b45ac291bfc 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,9 +1,4 @@ -""" -Support for HomematicIP Cloud components. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homematicip_cloud/ -""" +"""Support for HomematicIP Cloud devices.""" import logging import voluptuous as vol diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 3fdfc768c52..dc249775e3d 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -1,16 +1,9 @@ -""" -Support for HomematicIP Cloud alarm control panel. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/alarm_control_panel.homematicip_cloud/ -""" - +"""Support for HomematicIP Cloud alarm control panel.""" import logging from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.components.homematicip_cloud import ( - HMIPC_HAPID, HomematicipGenericDevice) -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 910666f93cb..9ed9d29ad39 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -1,15 +1,9 @@ -""" -Support for HomematicIP Cloud binary sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.homematicip_cloud/ -""" +"""Support for HomematicIP Cloud binary sensor.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.homematicip_cloud import ( - HMIPC_HAPID, HomematicipGenericDevice) -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) DEPENDENCIES = ['homematicip_cloud'] diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 966cd95ade1..2dff48774bb 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -1,17 +1,11 @@ -""" -Support for HomematicIP Cloud climate devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/climate.homematicip_cloud/ -""" +"""Support for HomematicIP Cloud climate devices.""" import logging from homeassistant.components.climate import ( ATTR_TEMPERATURE, STATE_AUTO, STATE_MANUAL, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) from homeassistant.components.homematicip_cloud import ( - HMIPC_HAPID, HomematicipGenericDevice) -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) from homeassistant.const import TEMP_CELSIUS _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index ea251a3bf87..458186bcce1 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -4,9 +4,9 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback -from .const import DOMAIN as HMIPC_DOMAIN -from .const import HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN -from .const import _LOGGER +from .const import ( + _LOGGER, DOMAIN as HMIPC_DOMAIN, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, + HMIPC_PIN) from .hap import HomematicipAuth diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 80fc8f7b430..7675929d40f 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -1,15 +1,9 @@ -""" -Support for HomematicIP Cloud cover devices. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.homematicip_cloud/ -""" +"""Support for HomematicIP Cloud cover devices.""" import logging -from homeassistant.components.cover import ( - ATTR_POSITION, CoverDevice) +from homeassistant.components.cover import ATTR_POSITION, CoverDevice from homeassistant.components.homematicip_cloud import ( - HMIPC_HAPID, HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN) + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) DEPENDENCIES = ['homematicip_cloud'] diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 9af6669652d..4acace4a8b1 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -2,8 +2,8 @@ import asyncio import logging -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 5d604d2c665..73c607683ba 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -1,9 +1,4 @@ -""" -Support for HomematicIP Cloud lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.homematicip_cloud/ -""" +"""Support for HomematicIP Cloud lights.""" import logging from homeassistant.components.homematicip_cloud import ( diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 911c00e45bc..b753a4d1aa5 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -1,16 +1,11 @@ -""" -Support for HomematicIP Cloud sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.homematicip_cloud/ -""" +"""Support for HomematicIP Cloud sensors.""" import logging from homeassistant.components.homematicip_cloud import ( DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) from homeassistant.const import ( - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index a1b3e1789bf..ad378074621 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -1,14 +1,8 @@ -""" -Support for HomematicIP Cloud switch. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.homematicip_cloud/ -""" +"""Support for HomematicIP Cloud switches.""" import logging from homeassistant.components.homematicip_cloud import ( - HMIPC_HAPID, HomematicipGenericDevice) -from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN + DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) from homeassistant.components.switch import SwitchDevice DEPENDENCIES = ['homematicip_cloud'] diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index b0510cfe9b5..d0769ed25e6 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -1,19 +1,15 @@ -"""Component for interfacing to Lutron Homeworks Series 4 and 8 systems. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homeworks/ -""" +"""Support for Lutron Homeworks Series 4 and 8 systems.""" import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( CONF_HOST, CONF_ID, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.dispatcher import ( - dispatcher_send, async_dispatcher_connect) + async_dispatcher_connect, dispatcher_send) from homeassistant.util import slugify REQUIREMENTS = ['pyhomeworks==0.0.6'] @@ -39,7 +35,7 @@ CV_FADE_RATE = vol.All(vol.Coerce(float), vol.Range(min=0, max=20)) DIMMER_SCHEMA = vol.Schema({ vol.Required(CONF_ADDR): cv.string, vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_RATE, default=FADE_RATE): CV_FADE_RATE + vol.Optional(CONF_RATE, default=FADE_RATE): CV_FADE_RATE, }) KEYPAD_SCHEMA = vol.Schema({ @@ -52,8 +48,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port, vol.Required(CONF_DIMMERS): vol.All(cv.ensure_list, [DIMMER_SCHEMA]), - vol.Optional(CONF_KEYPADS, default=[]): vol.All(cv.ensure_list, - [KEYPAD_SCHEMA]), + vol.Optional(CONF_KEYPADS, default=[]): + vol.All(cv.ensure_list, [KEYPAD_SCHEMA]), }), }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index 3ba5f805c52..7f5d7f6aab7 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -1,19 +1,14 @@ -"""Component for interfacing to Lutron Homeworks lights. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/light.homeworks/ -""" +"""Support for Lutron Homeworks lights.""" import logging from homeassistant.components.homeworks import ( - HomeworksDevice, HOMEWORKS_CONTROLLER, ENTITY_SIGNAL, - CONF_DIMMERS, CONF_ADDR, CONF_RATE) + CONF_ADDR, CONF_DIMMERS, CONF_RATE, ENTITY_SIGNAL, HOMEWORKS_CONTROLLER, + HomeworksDevice) from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) from homeassistant.const import CONF_NAME from homeassistant.core import callback -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect) +from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['homeworks'] diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 02b9affefd4..10d6ae5148b 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -1,9 +1,4 @@ -""" -This module provides WSGI application to serve the Home Assistant API. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/http/ -""" +"""Support to serve the Home Assistant API as WSGI application.""" from ipaddress import ip_network import logging import os @@ -18,17 +13,16 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, SERVER_PORT) import homeassistant.helpers.config_validation as cv import homeassistant.util as hass_util -from homeassistant.util.logging import HideSensitiveDataFilter from homeassistant.util import ssl as ssl_util +from homeassistant.util.logging import HideSensitiveDataFilter +# Import as alias from .auth import setup_auth from .ban import setup_bans +from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa from .cors import setup_cors from .real_ip import setup_real_ip from .static import CachingFileResponse, CachingStaticResource - -# Import as alias -from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa from .view import HomeAssistantView # noqa REQUIREMENTS = ['aiohttp_cors==0.7.0'] diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index a515fcd198e..312fc2164c3 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,5 +1,4 @@ """Authentication for HTTP component.""" - import base64 import hmac import logging @@ -8,20 +7,20 @@ from aiohttp import hdrs from aiohttp.web import middleware import jwt -from homeassistant.core import callback -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.auth.providers import legacy_api_password from homeassistant.auth.util import generate_secret +from homeassistant.const import HTTP_HEADER_HA_AUTH +from homeassistant.core import callback from homeassistant.util import dt as dt_util from .const import KEY_AUTHENTICATED, KEY_REAL_IP +_LOGGER = logging.getLogger(__name__) + DATA_API_PASSWORD = 'api_password' DATA_SIGN_SECRET = 'http.auth.sign_secret' SIGN_QUERY_PARAM = 'authSig' -_LOGGER = logging.getLogger(__name__) - @callback def async_sign_path(hass, refresh_token_id, path, expiration): diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 0d748c91c66..92c41157a33 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -9,11 +9,12 @@ from aiohttp.web import middleware from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized import voluptuous as vol -from homeassistant.core import callback, HomeAssistant from homeassistant.config import load_yaml_config_file +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.util.yaml import dump + from .const import KEY_REAL_IP _LOGGER = logging.getLogger(__name__) @@ -26,7 +27,7 @@ NOTIFICATION_ID_BAN = 'ip-ban' NOTIFICATION_ID_LOGIN = 'http-login' IP_BANS_FILE = 'ip_bans.yaml' -ATTR_BANNED_AT = "banned_at" +ATTR_BANNED_AT = 'banned_at' SCHEMA_IP_BAN_ENTRY = vol.Schema({ vol.Optional('banned_at'): vol.Any(None, cv.datetime) @@ -52,7 +53,7 @@ def setup_bans(hass, app, login_threshold): async def ban_middleware(request, handler): """IP Ban middleware.""" if KEY_BANNED_IPS not in request.app: - _LOGGER.error('IP Ban middleware loaded but banned IPs not loaded') + _LOGGER.error("IP Ban middleware loaded but banned IPs not loaded") return await handler(request) # Verify if IP is not banned diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 5698c6048e3..6da3b0e51d7 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -1,15 +1,10 @@ """Provide CORS support for the HTTP component.""" - - -from aiohttp.hdrs import ACCEPT, ORIGIN, CONTENT_TYPE +from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN from homeassistant.const import ( - HTTP_HEADER_X_REQUESTED_WITH, HTTP_HEADER_HA_AUTH) - - + HTTP_HEADER_HA_AUTH, HTTP_HEADER_X_REQUESTED_WITH) from homeassistant.core import callback - ALLOWED_CORS_HEADERS = [ ORIGIN, ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE, HTTP_HEADER_HA_AUTH] diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 8fc7cd8e658..98686e5cabd 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -1,5 +1,4 @@ """Decorator for view methods to help with data validation.""" - from functools import wraps import logging diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py index 27a8550ab8c..9bbf30bd9d1 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -1,9 +1,8 @@ """Middleware to fetch real IP.""" - from ipaddress import ip_address -from aiohttp.web import middleware from aiohttp.hdrs import X_FORWARDED_FOR +from aiohttp.web import middleware from homeassistant.core import callback diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index beb5c647266..9662f3e6c23 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -1,27 +1,21 @@ -""" -This module provides WSGI application to serve the Home Assistant API. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/http/ -""" +"""Support for views.""" import asyncio import json import logging from aiohttp import web from aiohttp.web_exceptions import ( - HTTPUnauthorized, HTTPInternalServerError, HTTPBadRequest) + HTTPBadRequest, HTTPInternalServerError, HTTPUnauthorized) import voluptuous as vol -from homeassistant.components.http.ban import process_success_login -from homeassistant.core import Context, is_callback -from homeassistant.const import CONTENT_TYPE_JSON from homeassistant import exceptions +from homeassistant.components.http.ban import process_success_login +from homeassistant.const import CONTENT_TYPE_JSON +from homeassistant.core import Context, is_callback from homeassistant.helpers.json import JSONEncoder from .const import KEY_AUTHENTICATED, KEY_REAL_IP - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 9d223df3344..2ff21c4d5a7 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -1,9 +1,4 @@ -""" -Support for Huawei LTE routers. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/huawei_lte/ -""" +"""Support for Huawei LTE routers.""" from datetime import timedelta from functools import reduce import logging @@ -18,7 +13,6 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle - _LOGGER = logging.getLogger(__name__) # dicttoxml (used by huawei-lte-api) has uselessly verbose INFO level. diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index d30a413898f..69bf42fb3fe 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -1,9 +1,4 @@ -""" -Support for Huawei LTE routers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.huawei_lte/ -""" +"""Support for device tracking of Huawei LTE routers.""" from typing import Any, Dict, List, Optional import attr @@ -16,7 +11,6 @@ from homeassistant.components.device_tracker import ( from homeassistant.const import CONF_URL from ..huawei_lte import DATA_KEY, RouterData - DEPENDENCIES = ['huawei_lte'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index a406a7ec2d8..5e20a774c25 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -1,9 +1,4 @@ -"""Huawei LTE router platform for notify component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.huawei_lte/ -""" - +"""Support for Huawei LTE router notifications.""" import logging import voluptuous as vol @@ -16,7 +11,6 @@ import homeassistant.helpers.config_validation as cv from ..huawei_lte import DATA_KEY - DEPENDENCIES = ['huawei_lte'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index ae376045544..42ad4b52f8d 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -1,9 +1,4 @@ -"""Huawei LTE sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.huawei_lte/ -""" - +"""Support for Huawei LTE sensors.""" import logging import re @@ -19,7 +14,6 @@ import homeassistant.helpers.config_validation as cv from ..huawei_lte import DATA_KEY, RouterData - _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['huawei_lte'] @@ -118,8 +112,8 @@ def setup_platform( sensors = [] for path in config.get(CONF_MONITORED_CONDITIONS): data.subscribe(path) - sensors.append(HuaweiLteSensor( - data, path, SENSOR_META.get(path, {}))) + sensors.append(HuaweiLteSensor(data, path, SENSOR_META.get(path, {}))) + add_entities(sensors, True) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 7618e702d04..e5f40e3ca29 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -1,9 +1,4 @@ -""" -This component provides basic support for the Philips Hue system. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/hue/ -""" +"""Support for the Philips Hue system.""" import ipaddress import logging diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 6e3d818db68..9df5b0a6730 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -46,7 +46,7 @@ class HueBridge: self.api = await get_bridge( hass, host, self.config_entry.data['username']) except AuthenticationRequired: - # usernames can become invalid if hub is reset or user removed. + # Usernames can become invalid if hub is reset or user removed. # We are going to fail the config entry setup and initiate a new # linking procedure. When linking succeeds, it will remove the # old config entry. diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 51e50f629b5..7518a5a381f 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -1,9 +1,4 @@ -""" -This component provides light support for the Philips Hue system. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.hue/ -""" +"""Support for the Philips Hue lights.""" import asyncio from datetime import timedelta import logging @@ -37,8 +32,8 @@ SUPPORT_HUE = { 'Color light': SUPPORT_HUE_COLOR, 'Dimmable light': SUPPORT_HUE_DIMMABLE, 'On/Off plug-in unit': SUPPORT_HUE_ON_OFF, - 'Color temperature light': SUPPORT_HUE_COLOR_TEMP - } + 'Color temperature light': SUPPORT_HUE_COLOR_TEMP, +} ATTR_IS_HUE_GROUP = 'is_hue_group' GAMUT_TYPE_UNAVAILABLE = 'None' @@ -49,8 +44,8 @@ GAMUT_TYPE_UNAVAILABLE = 'None' GROUP_MIN_API_VERSION = (1, 13, 0) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Old way of setting up Hue lights. Can only be called when a user accidentally mentions hue platform in their diff --git a/homeassistant/components/idteck_prox/__init__.py b/homeassistant/components/idteck_prox/__init__.py index 8ec6f49b95d..3de7aa7cc8c 100644 --- a/homeassistant/components/idteck_prox/__init__.py +++ b/homeassistant/components/idteck_prox/__init__.py @@ -37,7 +37,7 @@ def setup(hass, config): reader.connect() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, reader.stop) except OSError as error: - _LOGGER.error('Error creating "%s". %s', name, error) + _LOGGER.error("Error creating %s. %s", name, error) return False return True diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 7dee93b2260..0a06947b00f 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -5,9 +5,9 @@ import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyfttt==0.3'] DEPENDENCIES = ['webhook'] diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index bbb9a02c8a1..98a176b1e82 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -10,9 +10,9 @@ from homeassistant.components.alarm_control_panel import ( from homeassistant.components.ifttt import ( ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE, - CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) + ATTR_ENTITY_ID, ATTR_STATE, CONF_CODE, CONF_NAME, CONF_OPTIMISTIC, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['ifttt'] diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 823f9d2657d..3365942df45 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -1,9 +1,4 @@ -""" -Support for IHC devices. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ihc/ -""" +"""Support for IHC devices.""" import logging import os.path @@ -224,7 +219,7 @@ def autosetup_ihc_products(hass: HomeAssistantType, config, ihc_controller, return False project = ElementTree.fromstring(project_xml) - # if an auto setup file exist in the configuration it will override + # If an auto setup file exist in the configuration it will override yaml_path = hass.config.path(AUTO_SETUP_YAML) if not os.path.isfile(yaml_path): yaml_path = os.path.join(os.path.dirname(__file__), AUTO_SETUP_YAML) diff --git a/homeassistant/components/ihc/binary_sensor.py b/homeassistant/components/ihc/binary_sensor.py index fb5b4c0bfc2..7e3371a834c 100644 --- a/homeassistant/components/ihc/binary_sensor.py +++ b/homeassistant/components/ihc/binary_sensor.py @@ -1,17 +1,9 @@ -"""IHC binary sensor platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.ihc/ -""" -from homeassistant.components.binary_sensor import ( - BinarySensorDevice) -from homeassistant.components.ihc import ( - IHC_DATA, IHC_CONTROLLER, IHC_INFO) -from homeassistant.components.ihc.const import ( - CONF_INVERTING) +"""Support for IHC binary sensors.""" +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.ihc import IHC_CONTROLLER, IHC_DATA, IHC_INFO +from homeassistant.components.ihc.const import CONF_INVERTING from homeassistant.components.ihc.ihcdevice import IHCDevice -from homeassistant.const import ( - CONF_TYPE) +from homeassistant.const import CONF_TYPE DEPENDENCIES = ['ihc'] @@ -31,10 +23,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): info = hass.data[ihc_key][IHC_INFO] ihc_controller = hass.data[ihc_key][IHC_CONTROLLER] - sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info, - product_cfg.get(CONF_TYPE), - product_cfg[CONF_INVERTING], - product) + sensor = IHCBinarySensor( + ihc_controller, name, ihc_id, info, product_cfg.get(CONF_TYPE), + product_cfg[CONF_INVERTING], product) devices.append(sensor) add_entities(devices) diff --git a/homeassistant/components/ihc/light.py b/homeassistant/components/ihc/light.py index f80c9b2fd6f..2590ea83222 100644 --- a/homeassistant/components/ihc/light.py +++ b/homeassistant/components/ihc/light.py @@ -1,14 +1,8 @@ -"""IHC light platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.ihc/ -""" +"""Support for IHC lights.""" import logging -from homeassistant.components.ihc import ( - IHC_DATA, IHC_CONTROLLER, IHC_INFO) -from homeassistant.components.ihc.const import ( - CONF_DIMMABLE) +from homeassistant.components.ihc import IHC_CONTROLLER, IHC_DATA, IHC_INFO +from homeassistant.components.ihc.const import CONF_DIMMABLE from homeassistant.components.ihc.ihcdevice import IHCDevice from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) diff --git a/homeassistant/components/ihc/sensor.py b/homeassistant/components/ihc/sensor.py index f5a45599bb7..930ac221629 100644 --- a/homeassistant/components/ihc/sensor.py +++ b/homeassistant/components/ihc/sensor.py @@ -1,13 +1,7 @@ -"""IHC sensor platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.ihc/ -""" -from homeassistant.components.ihc import ( - IHC_DATA, IHC_CONTROLLER, IHC_INFO) +"""Support for IHC sensors.""" +from homeassistant.components.ihc import IHC_CONTROLLER, IHC_DATA, IHC_INFO from homeassistant.components.ihc.ihcdevice import IHCDevice -from homeassistant.const import ( - CONF_UNIT_OF_MEASUREMENT) +from homeassistant.const import CONF_UNIT_OF_MEASUREMENT from homeassistant.helpers.entity import Entity DEPENDENCIES = ['ihc'] @@ -28,8 +22,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): info = hass.data[ihc_key][IHC_INFO] ihc_controller = hass.data[ihc_key][IHC_CONTROLLER] unit = product_cfg[CONF_UNIT_OF_MEASUREMENT] - sensor = IHCSensor(ihc_controller, name, ihc_id, info, - unit, product) + sensor = IHCSensor(ihc_controller, name, ihc_id, info, unit, product) devices.append(sensor) add_entities(devices) diff --git a/homeassistant/components/ihc/switch.py b/homeassistant/components/ihc/switch.py index e217d109cbc..bbab9d3e68c 100644 --- a/homeassistant/components/ihc/switch.py +++ b/homeassistant/components/ihc/switch.py @@ -1,10 +1,5 @@ -"""IHC switch platform. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.ihc/ -""" -from homeassistant.components.ihc import ( - IHC_DATA, IHC_CONTROLLER, IHC_INFO) +"""Support for IHC switches.""" +from homeassistant.components.ihc import IHC_CONTROLLER, IHC_DATA, IHC_INFO from homeassistant.components.ihc.ihcdevice import IHCDevice from homeassistant.components.switch import SwitchDevice @@ -31,7 +26,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class IHCSwitch(IHCDevice, SwitchDevice): - """IHC Switch.""" + """Representation of an IHC switch.""" def __init__(self, ihc_controller, name: str, ihc_id: int, info: bool, product=None) -> None: diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index 0b1282b605a..737216af5c9 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -1,25 +1,18 @@ -""" -Native Home Assistant iOS app component. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/ecosystem/ios/ -""" -import logging +"""Native Home Assistant iOS app component.""" import datetime +import logging import voluptuous as vol from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView -from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR, - HTTP_BAD_REQUEST) +from homeassistant.const import HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( - config_validation as cv, discovery, config_entry_flow) + config_entry_flow, config_validation as cv, discovery) from homeassistant.util.json import load_json, save_json - _LOGGER = logging.getLogger(__name__) DOMAIN = 'ios' diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index e6a37d707ad..1f8aade4ec1 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -1,20 +1,14 @@ -""" -iOS push notification platform for notify component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/ecosystem/ios/notifications/ -""" -import logging +"""Support for iOS push notifications.""" from datetime import datetime, timezone +import logging + import requests from homeassistant.components import ios - -import homeassistant.util.dt as dt_util - from homeassistant.components.notify import ( - ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_MESSAGE, - ATTR_DATA, BaseNotificationService) + ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, + BaseNotificationService) +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -85,7 +79,7 @@ class iOSNotificationService(BaseNotificationService): for target in targets: if target not in ios.enabled_push_ids(self.hass): - _LOGGER.error("The target (%s) does not exist in .ios.conf.", + _LOGGER.error("The target (%s) does not exist in .ios.conf", targets) return diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index d206cd1df87..404b313368c 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -1,9 +1,4 @@ -""" -Support for Home Assistant iOS app sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/ecosystem/ios/ -""" +"""Support for Home Assistant iOS app sensors.""" from homeassistant.components import ios from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 87f62371b55..9bb54a1a019 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,9 +1,4 @@ -""" -Component for the Portuguese weather service - IPMA. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ipma/ -""" +"""Component for the Portuguese weather service - IPMA.""" from homeassistant.core import Config, HomeAssistant from .config_flow import IpmaFlowHandler # noqa from .const import DOMAIN # noqa @@ -13,7 +8,6 @@ DEFAULT_NAME = 'ipma' async def async_setup(hass: HomeAssistant, config: Config) -> bool: """Set up configured IPMA.""" - # No support for component configuration return True diff --git a/homeassistant/components/ipma/const.py b/homeassistant/components/ipma/const.py index dbb19294541..bdd97c74e6a 100644 --- a/homeassistant/components/ipma/const.py +++ b/homeassistant/components/ipma/const.py @@ -1,4 +1,4 @@ -"""Constants in ipma component.""" +"""Constants for IPMA component.""" import logging from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index ec9b6fec2e8..7122957ad12 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -1,9 +1,4 @@ -""" -Support for IPMA weather service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.ipma/ -""" +"""Support for IPMA weather service.""" import logging from datetime import timedelta @@ -54,13 +49,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the ipma platform. Deprecated. """ - _LOGGER.warning('Loading IPMA via platform config is deprecated') + _LOGGER.warning("Loading IPMA via platform config is deprecated") latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 2b5f8fcb13f..4eaa71deece 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,19 +1,14 @@ -""" -Support the ISY-994 controllers. - -For configuration details please visit the documentation for this component at -https://home-assistant.io/components/isy994/ -""" +"""Support the ISY-994 controllers.""" from collections import namedtuple import logging from urllib.parse import urlparse import voluptuous as vol -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.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, Dict @@ -46,8 +41,7 @@ CONFIG_SCHEMA = vol.Schema({ default=DEFAULT_IGNORE_STRING): cv.string, vol.Optional(CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING): cv.string, - vol.Optional(CONF_ENABLE_CLIMATE, - default=True): cv.boolean + vol.Optional(CONF_ENABLE_CLIMATE, default=True): cv.boolean, }) }, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index b5d676f233f..013b99fbb15 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -1,20 +1,15 @@ -""" -Support for ISY994 binary sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.isy994/ -""" -import logging +"""Support for ISY994 binary sensors.""" from datetime import timedelta +import logging from typing import Callable +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice +from homeassistant.components.isy994 import ( + ISY994_NODES, ISY994_PROGRAMS, ISYDevice) +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback -from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN -from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - ISYDevice) -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 4ead61e6b7a..22ea1629794 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -1,17 +1,12 @@ -""" -Support for ISY994 covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.isy994/ -""" +"""Support for ISY994 covers.""" import logging from typing import Callable -from homeassistant.components.cover import CoverDevice, DOMAIN -from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - ISYDevice) +from homeassistant.components.cover import DOMAIN, CoverDevice +from homeassistant.components.isy994 import ( + ISY994_NODES, ISY994_PROGRAMS, ISYDevice) from homeassistant.const import ( - STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING, STATE_UNKNOWN) + STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING, STATE_UNKNOWN) from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 314200ba1c4..142eaedd66b 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -1,17 +1,12 @@ -""" -Support for ISY994 fans. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/fan.isy994/ -""" +"""Support for ISY994 fans.""" import logging from typing import Callable -from homeassistant.components.fan import (FanEntity, DOMAIN, SPEED_OFF, - SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, SUPPORT_SET_SPEED) -from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - ISYDevice) +from homeassistant.components.fan import ( + DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, + FanEntity) +from homeassistant.components.isy994 import ( + ISY994_NODES, ISY994_PROGRAMS, ISYDevice) from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index d54aa3cd4ce..cc39a6d1a3b 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -1,15 +1,9 @@ -""" -Support for ISY994 lights. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.isy994/ -""" +"""Support for ISY994 lights.""" import logging from typing import Callable -from homeassistant.components.light import ( - Light, SUPPORT_BRIGHTNESS, DOMAIN) from homeassistant.components.isy994 import ISY994_NODES, ISYDevice +from homeassistant.components.light import DOMAIN, SUPPORT_BRIGHTNESS, Light from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 9481e619a61..a2e8b1a1e56 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -1,23 +1,18 @@ -""" -Support for ISY994 locks. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/lock.isy994/ -""" +"""Support for ISY994 locks.""" import logging from typing import Callable -from homeassistant.components.lock import LockDevice, DOMAIN -from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - ISYDevice) -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN +from homeassistant.components.isy994 import ( + ISY994_NODES, ISY994_PROGRAMS, ISYDevice) +from homeassistant.components.lock import DOMAIN, LockDevice +from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) VALUE_TO_STATE = { 0: STATE_UNLOCKED, - 100: STATE_LOCKED + 100: STATE_LOCKED, } diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index eca7e88a17e..60212d081de 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -1,17 +1,11 @@ -""" -Support for ISY994 sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.isy994/ -""" +"""Support for ISY994 sensors.""" import logging from typing import Callable +from homeassistant.components.isy994 import ( + ISY994_NODES, ISY994_WEATHER, ISYDevice) from homeassistant.components.sensor import DOMAIN -from homeassistant.components.isy994 import (ISY994_NODES, ISY994_WEATHER, - ISYDevice) -from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_UV_INDEX) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_UV_INDEX from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 6bb9c07de5b..96f17c80bef 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -1,15 +1,10 @@ -""" -Support for ISY994 switches. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.isy994/ -""" +"""Support for ISY994 switches.""" import logging from typing import Callable -from homeassistant.components.switch import SwitchDevice, DOMAIN -from homeassistant.components.isy994 import (ISY994_NODES, ISY994_PROGRAMS, - ISYDevice) +from homeassistant.components.isy994 import ( + ISY994_NODES, ISY994_PROGRAMS, ISYDevice) +from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py index 6dfff0bd02c..e756cbfa918 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -1,9 +1,4 @@ -""" -Support for locks through the SmartThings cloud API. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/smartthings.lock/ -""" +"""Support for locks through the SmartThings cloud API.""" from homeassistant.components.lock import LockDevice from . import SmartThingsEntity @@ -15,12 +10,12 @@ ST_STATE_LOCKED = 'locked' ST_LOCK_ATTR_MAP = { 'method': 'method', 'codeId': 'code_id', - 'timeout': 'timeout' + 'timeout': 'timeout', } -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Platform uses config entry setup.""" pass From dc62cb6a88f189f5a40ca022414512dc0d959a37 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 14 Feb 2019 16:42:03 +0100 Subject: [PATCH 004/253] Update file header --- homeassistant/components/water_heater/demo.py | 2 +- homeassistant/components/weather/__init__.py | 13 ++++------- homeassistant/components/weather/bom.py | 19 ++++++--------- .../components/weather/buienradar.py | 22 +++++++----------- homeassistant/components/weather/darksky.py | 18 +++++---------- homeassistant/components/weather/demo.py | 13 ++++------- homeassistant/components/weather/met.py | 15 ++++-------- .../components/weather/meteo_france.py | 19 +++++---------- homeassistant/components/weather/metoffice.py | 7 +----- .../components/weather/openweathermap.py | 16 ++++--------- homeassistant/components/weather/yweather.py | 7 +----- homeassistant/components/weather/zamg.py | 23 ++++++++----------- 12 files changed, 57 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/water_heater/demo.py b/homeassistant/components/water_heater/demo.py index a0220927f16..b551993aca5 100644 --- a/homeassistant/components/water_heater/demo.py +++ b/homeassistant/components/water_heater/demo.py @@ -1,4 +1,4 @@ -"""Demo platform that offers a fake water_heater device.""" +"""Demo platform that offers a fake water heater device.""" from homeassistant.components.water_heater import ( WaterHeaterDevice, SUPPORT_TARGET_TEMPERATURE, diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index d479725657b..34cd86347f2 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1,18 +1,13 @@ -""" -Weather component that handles meteorological data for your location. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/weather/ -""" +"""Weather component that handles meteorological data for your location.""" from datetime import timedelta import logging -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.temperature import display_temp as show_temp -from homeassistant.const import PRECISION_WHOLE, PRECISION_TENTHS, TEMP_CELSIUS +from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS from homeassistant.helpers.config_validation import ( # noqa PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.temperature import display_temp as show_temp _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/weather/bom.py b/homeassistant/components/weather/bom.py index 1ed54496c6f..b05d5fc594d 100644 --- a/homeassistant/components/weather/bom.py +++ b/homeassistant/components/weather/bom.py @@ -1,20 +1,15 @@ -""" -Support for Australian BOM (Bureau of Meteorology) weather service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.bom/ -""" +"""Support for Australian BOM (Bureau of Meteorology) weather service.""" import logging import voluptuous as vol -from homeassistant.components.weather import WeatherEntity, PLATFORM_SCHEMA -from homeassistant.const import \ - CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation -from homeassistant.components.sensor.bom import \ - BOMCurrentData, closest_station, CONF_STATION, validate_station +from homeassistant.components.sensor.bom import ( + CONF_STATION, BOMCurrentData, closest_station, validate_station) +from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity +from homeassistant.const import ( + CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) +from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py index 1ec3fc513e9..31f51824146 100644 --- a/homeassistant/components/weather/buienradar.py +++ b/homeassistant/components/weather/buienradar.py @@ -1,22 +1,16 @@ -""" -Support for Buienradar.nl weather service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.buienradar/ -""" +"""Support for Buienradar.nl weather service.""" import logging import voluptuous as vol -from homeassistant.components.weather import ( - WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) -from homeassistant.const import \ - CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation -from homeassistant.components.sensor.buienradar import ( - BrData) +from homeassistant.components.sensor.buienradar import BrData +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) +from homeassistant.const import ( + CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) +from homeassistant.helpers import config_validation as cv REQUIREMENTS = ['buienradar==0.91'] diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 4ac3d2a1d22..17e3cbbcf14 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -1,9 +1,4 @@ -""" -Platform for retrieving meteorological data from Dark Sky. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/weather.darksky/ -""" +"""Support for retrieving meteorological data from Dark Sky.""" from datetime import datetime, timedelta import logging @@ -12,13 +7,12 @@ from requests.exceptions import ( import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_WIND_SPEED, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_PRECIPITATION, - PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( - CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS, - CONF_MODE, TEMP_FAHRENHEIT) + CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME, + TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle diff --git a/homeassistant/components/weather/demo.py b/homeassistant/components/weather/demo.py index 6bcb8918504..d20e91b1f93 100644 --- a/homeassistant/components/weather/demo.py +++ b/homeassistant/components/weather/demo.py @@ -1,15 +1,10 @@ -""" -Demo platform that offers fake meteorological data. - -For more details about this platform, please refer to the documentation -https://home-assistant.io/components/demo/ -""" +"""Demo platform that offers fake meteorological data.""" from datetime import datetime, timedelta from homeassistant.components.weather import ( - WeatherEntity, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) -from homeassistant.const import (TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, WeatherEntity) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT CONDITION_CLASSES = { 'cloudy': [], diff --git a/homeassistant/components/weather/met.py b/homeassistant/components/weather/met.py index c905e6b6ce3..90978314a20 100644 --- a/homeassistant/components/weather/met.py +++ b/homeassistant/components/weather/met.py @@ -1,21 +1,16 @@ -""" -Support for Met.no weather service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.met/ -""" +"""Support for Met.no weather service.""" import logging from random import randrange import voluptuous as vol from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity -from homeassistant.const import (CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, - CONF_NAME, TEMP_CELSIUS) +from homeassistant.const import ( + CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.event import (async_track_utc_time_change, - async_call_later) +from homeassistant.helpers.event import ( + async_call_later, async_track_utc_time_change) import homeassistant.util.dt as dt_util REQUIREMENTS = ['pyMetno==0.4.5'] diff --git a/homeassistant/components/weather/meteo_france.py b/homeassistant/components/weather/meteo_france.py index a9d11d198f6..cf8b2b497fb 100644 --- a/homeassistant/components/weather/meteo_france.py +++ b/homeassistant/components/weather/meteo_france.py @@ -1,19 +1,12 @@ -""" -Support for Meteo france weather service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.meteo_france/ -""" -import logging +"""Support for Meteo france weather service.""" from datetime import datetime, timedelta +import logging -from homeassistant.components.meteo_france import (DATA_METEO_FRANCE, - CONDITION_CLASSES, - CONF_CITY, - ATTRIBUTION) +from homeassistant.components.meteo_france import ( + ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE) from homeassistant.components.weather import ( - WeatherEntity, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) + ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, WeatherEntity) from homeassistant.const import TEMP_CELSIUS _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/weather/metoffice.py b/homeassistant/components/weather/metoffice.py index 7382319e7a4..14738980482 100644 --- a/homeassistant/components/weather/metoffice.py +++ b/homeassistant/components/weather/metoffice.py @@ -1,9 +1,4 @@ -""" -Support for UK Met Office weather service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.metoffice/ -""" +"""Support for UK Met Office weather service.""" import logging import voluptuous as vol diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 2b86359361a..2100cf169b6 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -1,9 +1,4 @@ -""" -Support for the OpenWeatherMap (OWM) service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.openweathermap/ -""" +"""Support for the OpenWeatherMap (OWM) service.""" from datetime import timedelta import logging @@ -11,12 +6,11 @@ import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_SPEED, - ATTR_FORECAST_WIND_BEARING, - PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( - CONF_API_KEY, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, - CONF_NAME, STATE_UNKNOWN) + CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME, + STATE_UNKNOWN, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py index 567b8e235a8..e4eb34a039a 100644 --- a/homeassistant/components/weather/yweather.py +++ b/homeassistant/components/weather/yweather.py @@ -1,9 +1,4 @@ -""" -Support for the Yahoo! Weather service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.yweather/ -""" +"""Support for the Yahoo! Weather service.""" from datetime import timedelta import logging diff --git a/homeassistant/components/weather/zamg.py b/homeassistant/components/weather/zamg.py index f76b733ef0b..60707fa5e30 100644 --- a/homeassistant/components/weather/zamg.py +++ b/homeassistant/components/weather/zamg.py @@ -1,23 +1,18 @@ -""" -Sensor for data from Austrian "Zentralanstalt für Meteorologie und Geodynamik". - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/weather.zamg/ -""" +"""Sensor for data from Austrian Zentralanstalt für Meteorologie.""" import logging import voluptuous as vol -from homeassistant.components.weather import ( - WeatherEntity, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, - ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_SPEED, PLATFORM_SCHEMA) -from homeassistant.const import ( - CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE) -from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation from homeassistant.components.sensor.zamg import ( - ATTRIBUTION, closest_station, CONF_STATION_ID, zamg_stations, ZamgData) + ATTRIBUTION, CONF_STATION_ID, ZamgData, closest_station, zamg_stations) +from homeassistant.components.weather import ( + ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, PLATFORM_SCHEMA, + WeatherEntity) +from homeassistant.const import ( + CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) +from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) From c5de32e7b13df9f63b1279f733ea59d7ecec8243 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 14 Feb 2019 20:34:43 +0100 Subject: [PATCH 005/253] Climate const.py move (#20945) * Move constants to const.py * Import from const instead of climate --- homeassistant/components/alexa/smart_home.py | 9 +- homeassistant/components/climate/__init__.py | 34 +++---- homeassistant/components/climate/const.py | 28 ++++++ .../components/climate/coolmaster.py | 7 +- homeassistant/components/climate/demo.py | 5 +- homeassistant/components/climate/dyson.py | 5 +- homeassistant/components/climate/ephember.py | 8 +- .../components/climate/eq3btsmart.py | 9 +- homeassistant/components/climate/flexit.py | 5 +- .../components/climate/generic_thermostat.py | 7 +- homeassistant/components/climate/heatmiser.py | 5 +- homeassistant/components/climate/honeywell.py | 5 +- homeassistant/components/climate/melissa.py | 5 +- homeassistant/components/climate/mill.py | 5 +- homeassistant/components/climate/nuheat.py | 4 +- homeassistant/components/climate/oem.py | 11 +-- homeassistant/components/climate/proliphix.py | 9 +- .../components/climate/radiotherm.py | 10 ++- homeassistant/components/climate/sensibo.py | 5 +- homeassistant/components/climate/touchline.py | 5 +- homeassistant/components/climate/venstar.py | 8 +- .../components/climate/zhong_hong.py | 7 +- homeassistant/components/daikin/climate.py | 12 +-- homeassistant/components/ecobee/climate.py | 9 +- homeassistant/components/elkm1/climate.py | 10 ++- homeassistant/components/evohome/climate.py | 7 +- homeassistant/components/fritzbox/climate.py | 11 +-- .../components/google_assistant/trait.py | 9 +- homeassistant/components/hive/climate.py | 10 ++- .../components/homekit/type_thermostats.py | 2 +- .../components/homekit_controller/climate.py | 11 +-- homeassistant/components/homematic/climate.py | 5 +- .../components/homematicip_cloud/climate.py | 8 +- homeassistant/components/knx/climate.py | 7 +- homeassistant/components/maxcube/climate.py | 5 +- homeassistant/components/modbus/climate.py | 5 +- homeassistant/components/mqtt/climate.py | 6 +- homeassistant/components/mysensors/climate.py | 11 +-- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/nest/climate.py | 11 +-- homeassistant/components/nest/sensor.py | 2 +- homeassistant/components/netatmo/climate.py | 5 +- .../components/opentherm_gw/climate.py | 6 +- .../components/prometheus/__init__.py | 2 +- .../components/smartthings/climate.py | 11 +-- homeassistant/components/spider/climate.py | 9 +- homeassistant/components/tado/climate.py | 5 +- homeassistant/components/tesla/climate.py | 6 +- homeassistant/components/toon/climate.py | 9 +- homeassistant/components/tuya/climate.py | 9 +- homeassistant/components/velbus/climate.py | 5 +- homeassistant/components/vera/climate.py | 7 +- homeassistant/components/wink/climate.py | 11 +-- homeassistant/components/xs1/climate.py | 6 +- homeassistant/components/zwave/climate.py | 5 +- homeassistant/helpers/state.py | 2 +- tests/components/climate/common.py | 5 +- tests/components/climate/test_demo.py | 14 +-- .../climate/test_generic_thermostat.py | 67 +++++++------- tests/components/climate/test_honeywell.py | 2 +- tests/components/climate/test_melissa.py | 5 +- tests/components/climate/test_nuheat.py | 2 +- .../climate/test_reproduce_state.py | 4 +- tests/components/ecobee/test_climate.py | 2 +- .../google_assistant/test_google_assistant.py | 3 +- .../google_assistant/test_smart_home.py | 12 +-- .../components/google_assistant/test_trait.py | 12 +-- .../homekit/test_type_thermostats.py | 2 +- .../homekit_controller/test_climate.py | 2 +- tests/components/mqtt/test_climate.py | 90 ++++++++++--------- tests/components/smartthings/test_climate.py | 6 +- tests/components/zwave/test_climate.py | 2 +- 72 files changed, 372 insertions(+), 295 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 4e2383bb43d..a856a3d8e82 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -11,8 +11,9 @@ import aiohttp import async_timeout from homeassistant.components import ( - alert, automation, binary_sensor, climate, cover, fan, group, http, + alert, automation, binary_sensor, cover, fan, group, http, input_boolean, light, lock, media_player, scene, script, sensor, switch) +from homeassistant.components.climate import const as climate from homeassistant.helpers import aiohttp_client from homeassistant.helpers.event import async_track_state_change from homeassistant.const import ( @@ -22,7 +23,7 @@ from homeassistant.const import ( SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_UP, SERVICE_VOLUME_SET, - SERVICE_VOLUME_MUTE, STATE_LOCKED, STATE_ON, STATE_UNAVAILABLE, + SERVICE_VOLUME_MUTE, STATE_LOCKED, STATE_ON, STATE_OFF, STATE_UNAVAILABLE, STATE_UNLOCKED, TEMP_CELSIUS, TEMP_FAHRENHEIT, MATCH_ALL) import homeassistant.core as ha import homeassistant.util.color as color_util @@ -58,7 +59,7 @@ API_THERMOSTAT_MODES = OrderedDict([ (climate.STATE_AUTO, 'AUTO'), (climate.STATE_ECO, 'ECO'), (climate.STATE_MANUAL, 'AUTO'), - (climate.STATE_OFF, 'OFF'), + (STATE_OFF, 'OFF'), (climate.STATE_IDLE, 'OFF'), (climate.STATE_FAN_ONLY, 'OFF'), (climate.STATE_DRY, 'OFF'), @@ -765,7 +766,7 @@ class _AlexaThermostatController(_AlexaInterface): unit = self.hass.config.units.temperature_unit if name == 'targetSetpoint': - temp = self.entity.attributes.get(climate.ATTR_TEMPERATURE) + temp = self.entity.attributes.get(ATTR_TEMPERATURE) elif name == 'lowerSetpoint': temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW) elif name == 'upperSetpoint': diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index e1d3093995c..0283359b1f2 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -51,6 +51,17 @@ from .const import ( SERVICE_SET_OPERATION_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_HUMIDITY_HIGH, + SUPPORT_TARGET_HUMIDITY_LOW, + SUPPORT_FAN_MODE, + SUPPORT_OPERATION_MODE, + SUPPORT_HOLD_MODE, + SUPPORT_SWING_MODE, + SUPPORT_AWAY_MODE, + SUPPORT_AUX_HEAT, ) from .reproduce_state import async_reproduce_states # noqa @@ -62,29 +73,6 @@ DEFAULT_MAX_HUMIDITY = 99 ENTITY_ID_FORMAT = DOMAIN + '.{}' SCAN_INTERVAL = timedelta(seconds=60) -STATE_HEAT = 'heat' -STATE_COOL = 'cool' -STATE_IDLE = 'idle' -STATE_AUTO = 'auto' -STATE_MANUAL = 'manual' -STATE_DRY = 'dry' -STATE_FAN_ONLY = 'fan_only' -STATE_ECO = 'eco' - -SUPPORT_TARGET_TEMPERATURE = 1 -SUPPORT_TARGET_TEMPERATURE_HIGH = 2 -SUPPORT_TARGET_TEMPERATURE_LOW = 4 -SUPPORT_TARGET_HUMIDITY = 8 -SUPPORT_TARGET_HUMIDITY_HIGH = 16 -SUPPORT_TARGET_HUMIDITY_LOW = 32 -SUPPORT_FAN_MODE = 64 -SUPPORT_OPERATION_MODE = 128 -SUPPORT_HOLD_MODE = 256 -SUPPORT_SWING_MODE = 512 -SUPPORT_AWAY_MODE = 1024 -SUPPORT_AUX_HEAT = 2048 -SUPPORT_ON_OFF = 4096 - CONVERTIBLE_ATTRIBUTE = [ ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index 2f84ee27bbd..e213ae09de6 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -20,6 +20,11 @@ ATTR_TARGET_TEMP_HIGH = 'target_temp_high' ATTR_TARGET_TEMP_LOW = 'target_temp_low' ATTR_TARGET_TEMP_STEP = 'target_temp_step' +DEFAULT_MIN_TEMP = 7 +DEFAULT_MAX_TEMP = 35 +DEFAULT_MIN_HUMITIDY = 30 +DEFAULT_MAX_HUMIDITY = 99 + DOMAIN = 'climate' SERVICE_SET_AUX_HEAT = 'set_aux_heat' @@ -30,3 +35,26 @@ SERVICE_SET_HUMIDITY = 'set_humidity' SERVICE_SET_OPERATION_MODE = 'set_operation_mode' SERVICE_SET_SWING_MODE = 'set_swing_mode' SERVICE_SET_TEMPERATURE = 'set_temperature' + +STATE_HEAT = 'heat' +STATE_COOL = 'cool' +STATE_IDLE = 'idle' +STATE_AUTO = 'auto' +STATE_MANUAL = 'manual' +STATE_DRY = 'dry' +STATE_FAN_ONLY = 'fan_only' +STATE_ECO = 'eco' + +SUPPORT_TARGET_TEMPERATURE = 1 +SUPPORT_TARGET_TEMPERATURE_HIGH = 2 +SUPPORT_TARGET_TEMPERATURE_LOW = 4 +SUPPORT_TARGET_HUMIDITY = 8 +SUPPORT_TARGET_HUMIDITY_HIGH = 16 +SUPPORT_TARGET_HUMIDITY_LOW = 32 +SUPPORT_FAN_MODE = 64 +SUPPORT_OPERATION_MODE = 128 +SUPPORT_HOLD_MODE = 256 +SUPPORT_SWING_MODE = 512 +SUPPORT_AWAY_MODE = 1024 +SUPPORT_AUX_HEAT = 2048 +SUPPORT_ON_OFF = 4096 diff --git a/homeassistant/components/climate/coolmaster.py b/homeassistant/components/climate/coolmaster.py index 32c77b93eea..fd00c9f22c4 100644 --- a/homeassistant/components/climate/coolmaster.py +++ b/homeassistant/components/climate/coolmaster.py @@ -9,10 +9,11 @@ import logging import voluptuous as vol -from homeassistant.components.climate import ( - PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, ClimateDevice) + SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index 14c22cefbe9..5b4775982a6 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -4,8 +4,9 @@ Demo platform that offers a fake climate device. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -from homeassistant.components.climate import ( - ClimateDevice, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_FAN_MODE, diff --git a/homeassistant/components/climate/dyson.py b/homeassistant/components/climate/dyson.py index 0b09ec7f0b4..09196a82bed 100644 --- a/homeassistant/components/climate/dyson.py +++ b/homeassistant/components/climate/dyson.py @@ -7,8 +7,9 @@ https://home-assistant.io/components/climate.dyson/ import logging from homeassistant.components.dyson import DYSON_DEVICES -from homeassistant.components.climate import ( - ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_COOL, STATE_IDLE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE) from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE diff --git a/homeassistant/components/climate/ephember.py b/homeassistant/components/climate/ephember.py index cd410cf3be4..9884d81a199 100644 --- a/homeassistant/components/climate/ephember.py +++ b/homeassistant/components/climate/ephember.py @@ -8,12 +8,12 @@ import logging from datetime import timedelta import voluptuous as vol -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_OFF, - STATE_AUTO, SUPPORT_AUX_HEAT, SUPPORT_OPERATION_MODE, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_AUTO, SUPPORT_AUX_HEAT, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( - TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD, ATTR_TEMPERATURE) + ATTR_TEMPERATURE, TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD, STATE_OFF) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyephember==0.2.0'] diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 1eaaaa9d34e..c7c5973fb86 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -8,13 +8,14 @@ import logging import voluptuous as vol -from homeassistant.components.climate import ( - STATE_ON, STATE_OFF, STATE_HEAT, STATE_MANUAL, STATE_ECO, PLATFORM_SCHEMA, - ClimateDevice, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_MANUAL, STATE_ECO, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_ON_OFF) from homeassistant.const import ( - CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) + ATTR_TEMPERATURE, CONF_MAC, CONF_DEVICES, STATE_ON, STATE_OFF, + TEMP_CELSIUS, PRECISION_HALVES) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.45'] diff --git a/homeassistant/components/climate/flexit.py b/homeassistant/components/climate/flexit.py index e0453b8bf90..fe7b5ff8e7c 100644 --- a/homeassistant/components/climate/flexit.py +++ b/homeassistant/components/climate/flexit.py @@ -17,8 +17,9 @@ import voluptuous as vol from homeassistant.const import ( CONF_NAME, CONF_SLAVE, TEMP_CELSIUS, ATTR_TEMPERATURE, DEVICE_DEFAULT_NAME) -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE) from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index ffab50c989d..da4f79ec1e6 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -11,10 +11,11 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.core import DOMAIN as HA_DOMAIN -from homeassistant.components.climate import ( - STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA) + SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_UNKNOWN, PRECISION_HALVES, diff --git a/homeassistant/components/climate/heatmiser.py b/homeassistant/components/climate/heatmiser.py index a03d1567e01..ff495706be7 100644 --- a/homeassistant/components/climate/heatmiser.py +++ b/homeassistant/components/climate/heatmiser.py @@ -8,8 +8,9 @@ import logging import voluptuous as vol -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_PORT, CONF_NAME, CONF_ID) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index e0f104a84b1..dbcbebff566 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -12,8 +12,9 @@ import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, ATTR_FAN_MODE, ATTR_FAN_LIST, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, ATTR_FAN_LIST, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE) from homeassistant.const import ( diff --git a/homeassistant/components/climate/melissa.py b/homeassistant/components/climate/melissa.py index 25beedfe0dd..b9eb28a61d7 100644 --- a/homeassistant/components/climate/melissa.py +++ b/homeassistant/components/climate/melissa.py @@ -6,8 +6,9 @@ https://home-assistant.io/components/climate.melissa/ """ import logging -from homeassistant.components.climate import ( - ClimateDevice, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_ON_OFF, STATE_AUTO, STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, SUPPORT_FAN_MODE ) diff --git a/homeassistant/components/climate/mill.py b/homeassistant/components/climate/mill.py index b735927cb80..6867f57ee48 100644 --- a/homeassistant/components/climate/mill.py +++ b/homeassistant/components/climate/mill.py @@ -9,8 +9,9 @@ import logging import voluptuous as vol -from homeassistant.components.climate import ( - ClimateDevice, DOMAIN, PLATFORM_SCHEMA, STATE_HEAT, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + DOMAIN, STATE_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE) from homeassistant.const import ( diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py index d0bfe5add58..f52d2c7b501 100644 --- a/homeassistant/components/climate/nuheat.py +++ b/homeassistant/components/climate/nuheat.py @@ -9,8 +9,8 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.components.climate import ( - ClimateDevice, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( DOMAIN, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, diff --git a/homeassistant/components/climate/oem.py b/homeassistant/components/climate/oem.py index e006242331c..f1e03396b05 100644 --- a/homeassistant/components/climate/oem.py +++ b/homeassistant/components/climate/oem.py @@ -13,11 +13,12 @@ import requests import voluptuous as vol # Import the device class from the component that you want to support -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, ATTR_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE) -from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, - CONF_PORT, TEMP_CELSIUS, CONF_NAME) +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_IDLE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE) +from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_HOST, CONF_USERNAME, CONF_PASSWORD, + CONF_PORT, TEMP_CELSIUS, CONF_NAME) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['oemthermostat==1.1'] diff --git a/homeassistant/components/climate/proliphix.py b/homeassistant/components/climate/proliphix.py index 76160a28c6e..c88ece033df 100644 --- a/homeassistant/components/climate/proliphix.py +++ b/homeassistant/components/climate/proliphix.py @@ -6,11 +6,12 @@ https://home-assistant.io/components/climate.proliphix/ """ import voluptuous as vol -from homeassistant.components.climate import ( - PRECISION_TENTHS, STATE_COOL, STATE_HEAT, STATE_IDLE, - ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_COOL, STATE_HEAT, STATE_IDLE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, PRECISION_TENTHS, TEMP_FAHRENHEIT, + ATTR_TEMPERATURE) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['proliphix==0.4.1'] diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index a72bf711242..bad20884536 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -9,12 +9,14 @@ import logging import voluptuous as vol -from homeassistant.components.climate import ( - STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_ON, STATE_OFF, - ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE) from homeassistant.const import ( - CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, PRECISION_HALVES) + ATTR_TEMPERATURE, CONF_HOST, PRECISION_HALVES, TEMP_FAHRENHEIT, STATE_ON, + STATE_OFF) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['radiotherm==2.0.0'] diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index bf1cf5bf345..7850b08fd6b 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -15,8 +15,9 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, STATE_ON, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from homeassistant.components.climate import ( - ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + ATTR_CURRENT_HUMIDITY, DOMAIN, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_ON_OFF, STATE_HEAT, STATE_COOL, STATE_FAN_ONLY, STATE_DRY, diff --git a/homeassistant/components/climate/touchline.py b/homeassistant/components/climate/touchline.py index 641f6e9a1d8..fa38bd37c8f 100644 --- a/homeassistant/components/climate/touchline.py +++ b/homeassistant/components/climate/touchline.py @@ -8,8 +8,9 @@ import logging import voluptuous as vol -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/climate/venstar.py b/homeassistant/components/climate/venstar.py index 16c0b206154..820443ee186 100644 --- a/homeassistant/components/climate/venstar.py +++ b/homeassistant/components/climate/venstar.py @@ -8,14 +8,14 @@ import logging import voluptuous as vol -from homeassistant.components.climate import ( +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE, + STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_AWAY_MODE, SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_HOLD_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, - ClimateDevice) + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_TIMEOUT, CONF_USERNAME, PRECISION_WHOLE, STATE_OFF, STATE_ON, TEMP_CELSIUS, diff --git a/homeassistant/components/climate/zhong_hong.py b/homeassistant/components/climate/zhong_hong.py index b564e9d1fa4..78cd7d16c48 100644 --- a/homeassistant/components/climate/zhong_hong.py +++ b/homeassistant/components/climate/zhong_hong.py @@ -8,10 +8,11 @@ import logging import voluptuous as vol -from homeassistant.components.climate import ( - ATTR_OPERATION_MODE, PLATFORM_SCHEMA, STATE_COOL, STATE_DRY, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + ATTR_OPERATION_MODE, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import (ATTR_TEMPERATURE, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index d97b506e273..775e4a216e5 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -4,17 +4,17 @@ import re import voluptuous as vol -from homeassistant.components.climate import ( +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_OPERATION_MODE, - ATTR_SWING_MODE, PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_DRY, - STATE_FAN_ONLY, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, - SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, - ClimateDevice) + ATTR_SWING_MODE, STATE_AUTO, STATE_COOL, STATE_DRY, + STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, + SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.daikin import DOMAIN as DAIKIN_DOMAIN from homeassistant.components.daikin.const import ( ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, ATTR_TARGET_TEMPERATURE) from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, TEMP_CELSIUS) + ATTR_TEMPERATURE, CONF_HOST, CONF_NAME, STATE_OFF, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index aa6440894e1..bfc67e7cfaf 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -4,15 +4,16 @@ import logging import voluptuous as vol from homeassistant.components import ecobee -from homeassistant.components.climate import ( - DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE, - SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF) + SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv _CONFIGURING = {} diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 467d542ee6d..72f93b5419c 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -1,12 +1,14 @@ """Support for control of Elk-M1 connected thermostats.""" -from homeassistant.components.climate import ( - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PRECISION_WHOLE, STATE_AUTO, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, STATE_AUTO, STATE_COOL, STATE_FAN_ONLY, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE_HIGH, - SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice) + SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.elkm1 import ( DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities) -from homeassistant.const import STATE_ON +from homeassistant.const import ( + STATE_ON, PRECISION_WHOLE) DEPENDENCIES = [ELK_DOMAIN] diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index ef82a3dc81c..955b82e37e3 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -4,13 +4,13 @@ import logging from requests.exceptions import HTTPError -from homeassistant.components.climate import ( - STATE_AUTO, STATE_ECO, STATE_MANUAL, STATE_OFF, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_ECO, STATE_MANUAL, SUPPORT_AWAY_MODE, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - ClimateDevice ) from homeassistant.components.evohome import ( DATA_EVOHOME, DISPATCHER_EVOHOME, @@ -22,6 +22,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, HTTP_TOO_MANY_REQUESTS, PRECISION_HALVES, + STATE_OFF, TEMP_CELSIUS ) from homeassistant.core import callback diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 64d99ebf133..e8c20061b4e 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -8,13 +8,14 @@ from homeassistant.components.fritzbox import ( ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_HOLIDAY_MODE, ATTR_STATE_LOCKED, ATTR_STATE_SUMMER_MODE, ATTR_STATE_WINDOW_OPEN) -from homeassistant.components.climate import ( - ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL, - STATE_OFF, STATE_ON, SUPPORT_OPERATION_MODE, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + ATTR_OPERATION_MODE, STATE_ECO, STATE_HEAT, STATE_MANUAL, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS) - + ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS, + STATE_OFF, STATE_ON) DEPENDENCIES = ['fritzbox'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index af2d726328e..d0368ee0775 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -2,7 +2,6 @@ import logging from homeassistant.components import ( - climate, cover, group, fan, @@ -15,6 +14,7 @@ from homeassistant.components import ( switch, vacuum, ) +from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -24,6 +24,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, ) from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.util import color as color_util, temperature as temp_util @@ -516,7 +517,7 @@ class TemperatureSettingTrait(_Trait): hass_to_google = { climate.STATE_HEAT: 'heat', climate.STATE_COOL: 'cool', - climate.STATE_OFF: 'off', + STATE_OFF: 'off', climate.STATE_AUTO: 'heatcool', climate.STATE_FAN_ONLY: 'fan-only', climate.STATE_DRY: 'dry', @@ -576,7 +577,7 @@ class TemperatureSettingTrait(_Trait): round(temp_util.convert(attrs[climate.ATTR_TARGET_TEMP_LOW], unit, TEMP_CELSIUS), 1) else: - target_temp = attrs.get(climate.ATTR_TEMPERATURE) + target_temp = attrs.get(ATTR_TEMPERATURE) if target_temp is not None: response['thermostatTemperatureSetpoint'] = round( temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1) @@ -606,7 +607,7 @@ class TemperatureSettingTrait(_Trait): await self.hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: self.state.entity_id, - climate.ATTR_TEMPERATURE: temp + ATTR_TEMPERATURE: temp }, blocking=True) elif command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE: diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 2ecc195061a..45829cda087 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,9 +1,11 @@ """Support for the Hive climate devices.""" -from homeassistant.components.climate import ( - STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON, SUPPORT_AUX_HEAT, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_HEAT, + SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) from homeassistant.components.hive import DATA_HIVE, DOMAIN -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_TEMPERATURE, STATE_OFF, STATE_ON, TEMP_CELSIUS) DEPENDENCIES = ['hive'] diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 4a9ccdee009..85cf7938fbd 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -3,7 +3,7 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 5531b266d21..ceadcd46b9d 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -1,12 +1,13 @@ """Support for Homekit climate devices.""" import logging -from homeassistant.components.climate import ( - STATE_COOL, STATE_HEAT, STATE_IDLE, SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, ClimateDevice) +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_COOL, STATE_IDLE, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) from homeassistant.components.homekit_controller import ( - KNOWN_ACCESSORIES, HomeKitEntity) -from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS + HomeKitEntity, KNOWN_ACCESSORIES) +from homeassistant.const import TEMP_CELSIUS, STATE_OFF, ATTR_TEMPERATURE DEPENDENCIES = ['homekit_controller'] diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 4fc4b190808..e5eb292b4ff 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -1,9 +1,10 @@ """Support for Homematic thermostats.""" import logging -from homeassistant.components.climate import ( +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( STATE_AUTO, STATE_MANUAL, SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, ClimateDevice) + SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.homematic import ( ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT, HMDevice) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 2dff48774bb..08c88bbb796 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -1,12 +1,12 @@ """Support for HomematicIP Cloud climate devices.""" import logging -from homeassistant.components.climate import ( - ATTR_TEMPERATURE, STATE_AUTO, STATE_MANUAL, SUPPORT_TARGET_TEMPERATURE, - ClimateDevice) +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_MANUAL, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.homematicip_cloud import ( DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice) -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 82eaa52ae5a..7e172287d4d 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -1,11 +1,12 @@ """Support for KNX/IP climate devices.""" import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.climate import ( - PLATFORM_SCHEMA, SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, STATE_HEAT, STATE_IDLE, STATE_MANUAL, STATE_DRY, - STATE_FAN_ONLY, STATE_ECO, ClimateDevice) + STATE_FAN_ONLY, STATE_ECO) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS) from homeassistant.core import callback diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index f5c4533123f..170a3ba349c 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -2,8 +2,9 @@ import socket import logging -from homeassistant.components.climate import ( - ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_AUTO, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) from homeassistant.components.maxcube import DATA_KEY from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index ed8cbda863f..860816cb63e 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -6,8 +6,9 @@ import voluptuous as vol from homeassistant.const import ( CONF_NAME, CONF_SLAVE, ATTR_TEMPERATURE) -from homeassistant.components.climate import ( - ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index c028ca5a6f6..957e1aadfb7 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -10,11 +10,13 @@ import voluptuous as vol from homeassistant.components import climate, mqtt from homeassistant.components.climate import ( + ClimateDevice, PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA) +from homeassistant.components.climate.const import ( ATTR_OPERATION_MODE, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, - PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, + STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) + SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.components.mqtt import ( ATTR_DISCOVERY_HASH, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index 20d608e1ca5..f8c52f65cda 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -1,12 +1,13 @@ """MySensors platform that offers a Climate (MySensors-HVAC) component.""" from homeassistant.components import mysensors -from homeassistant.components.climate import ( +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, STATE_AUTO, - STATE_COOL, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, + STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, - ClimateDevice) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.const import ( + ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) DICT_HA_TO_MYS = { STATE_AUTO: 'AutoChangeOver', diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index fe6a34cf404..21aaa2109a1 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -7,7 +7,7 @@ import threading import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( ATTR_AWAY_MODE, SERVICE_SET_AWAY_MODE) from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_FILENAME, CONF_MONITORED_CONDITIONS, diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 8746a1959ae..88b6cbbbeb0 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -5,14 +5,15 @@ import voluptuous as vol from homeassistant.components.nest import ( DATA_NEST, SIGNAL_NEST_UPDATE, DOMAIN as NEST_DOMAIN) -from homeassistant.components.climate import ( - STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, - PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE) from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, + ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF) from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py index 10fa83d23e0..bde3f681c2b 100644 --- a/homeassistant/components/nest/sensor.py +++ b/homeassistant/components/nest/sensor.py @@ -1,7 +1,7 @@ """Support for Nest Thermostat sensors.""" import logging -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( STATE_COOL, STATE_HEAT) from homeassistant.components.nest import ( DATA_NEST, DATA_NEST_CONFIG, CONF_SENSORS, NestSensorDevice) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 2b9bcbebaf2..1e16f2d3e05 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -4,8 +4,9 @@ from datetime import timedelta import voluptuous as vol from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE -from homeassistant.components.climate import ( - STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA, +from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate.const import ( + STATE_HEAT, STATE_IDLE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE) from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index ff6acc1a884..584be4c0c64 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -1,9 +1,9 @@ """Support for OpenTherm Gateway climate devices.""" import logging -from homeassistant.components.climate import (ClimateDevice, STATE_IDLE, - STATE_HEAT, STATE_COOL, - SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_IDLE, STATE_HEAT, STATE_COOL, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.opentherm_gw import ( CONF_FLOOR_TEMP, CONF_PRECISION, DATA_DEVICE, DATA_GW_VARS, DATA_OPENTHERM_GW, SIGNAL_OPENTHERM_GW_UPDATE) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 4508611e51b..65c1fbc4eb0 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -5,7 +5,7 @@ from aiohttp import web import voluptuous as vol from homeassistant import core as hacore -from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE +from homeassistant.components.climate.const import ATTR_CURRENT_TEMPERATURE from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_TEXT_PLAIN, diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 9340bcef337..d70d865202d 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -1,13 +1,14 @@ """Support for climate devices through the SmartThings cloud API.""" import asyncio -from homeassistant.components.climate import ( +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_TEMPERATURE, STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_OFF, + STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, - ClimateDevice) -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.const import ( + ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 08af44ad1ad..b3380ec8fb4 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -2,12 +2,13 @@ import logging -from homeassistant.components.climate import ( - ATTR_TEMPERATURE, STATE_COOL, STATE_HEAT, STATE_IDLE, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_COOL, STATE_HEAT, STATE_IDLE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, ClimateDevice) + SUPPORT_FAN_MODE) from homeassistant.components.spider import DOMAIN as SPIDER_DOMAIN -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS DEPENDENCIES = ['spider'] diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 1812d36b7cd..d5f152bbd76 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -2,8 +2,9 @@ import logging from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS) -from homeassistant.components.climate import ( - ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) from homeassistant.util.temperature import convert as convert_temperature from homeassistant.const import ATTR_TEMPERATURE from homeassistant.components.tado import DATA_TADO diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index 302c0006bcf..118e7204bca 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -1,9 +1,9 @@ """Support for Tesla HVAC system.""" import logging -from homeassistant.components.climate import ( - ENTITY_ID_FORMAT, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - ClimateDevice) +from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT +from homeassistant.components.climate.const import ( + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN from homeassistant.components.tesla import TeslaDevice from homeassistant.const import ( diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index 3397e3dacc2..c07ccf79d26 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -1,9 +1,10 @@ """Support for Toon van Eneco Thermostats.""" -from homeassistant.components.climate import ( - ATTR_TEMPERATURE, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_AUTO, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_COOL, STATE_ECO, STATE_HEAT, STATE_AUTO, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) import homeassistant.components.toon as toon_main -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 97ff18ba911..06714760a02 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -1,13 +1,14 @@ """Support for the Tuya climate devices.""" -from homeassistant.components.climate import ( - ATTR_TEMPERATURE, ENTITY_ID_FORMAT, STATE_AUTO, STATE_COOL, STATE_ECO, +from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_COOL, STATE_ECO, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH from homeassistant.components.tuya import DATA_TUYA, TuyaDevice from homeassistant.const import ( - PRECISION_WHOLE, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS, TEMP_FAHRENHEIT) DEPENDENCIES = ['tuya'] DEVICE_TYPE = 'climate' diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index ae7a2828492..1f45408a666 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -1,8 +1,9 @@ """Support for Velbus thermostat.""" import logging -from homeassistant.components.climate import ( - STATE_HEAT, SUPPORT_TARGET_TEMPERATURE, ClimateDevice) +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + STATE_HEAT, SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.velbus import ( DOMAIN as VELBUS_DOMAIN, VelbusEntity) from homeassistant.const import ( diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 7cd3129bc14..9c812da9208 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -2,9 +2,10 @@ import logging from homeassistant.util import convert -from homeassistant.components.climate import ( - ClimateDevice, STATE_AUTO, STATE_COOL, - STATE_HEAT, ENTITY_ID_FORMAT, SUPPORT_TARGET_TEMPERATURE, +from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT +from homeassistant.components.climate.const import ( + STATE_AUTO, STATE_COOL, + STATE_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE) from homeassistant.const import ( STATE_ON, diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py index 8d946bf03df..efd8eecf5af 100644 --- a/homeassistant/components/wink/climate.py +++ b/homeassistant/components/wink/climate.py @@ -1,17 +1,18 @@ """Support for Wink thermostats and Air Conditioners.""" import logging -from homeassistant.components.climate import ( +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( ATTR_CURRENT_HUMIDITY, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_TEMPERATURE, STATE_AUTO, STATE_COOL, STATE_ECO, + STATE_AUTO, STATE_COOL, STATE_ECO, STATE_FAN_ONLY, STATE_HEAT, SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, - ClimateDevice) + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.const import ( - PRECISION_TENTHS, STATE_OFF, STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS) + ATTR_TEMPERATURE, PRECISION_TENTHS, STATE_OFF, STATE_ON, STATE_UNKNOWN, + TEMP_CELSIUS) from homeassistant.helpers.temperature import display_temp as show_temp _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py index 92a2c75895c..e579761474b 100644 --- a/homeassistant/components/xs1/climate.py +++ b/homeassistant/components/xs1/climate.py @@ -2,10 +2,12 @@ from functools import partial import logging -from homeassistant.components.climate import ( - ATTR_TEMPERATURE, ClimateDevice, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE) from homeassistant.components.xs1 import ( ACTUATORS, DOMAIN as COMPONENT_DOMAIN, SENSORS, XS1DeviceEntity) +from homeassistant.const import ATTR_TEMPERATURE DEPENDENCIES = ['xs1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index bf7b64549ac..b0ab273e86a 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -2,8 +2,9 @@ # Because we do not compile openzwave on CI import logging from homeassistant.core import callback -from homeassistant.components.climate import ( - DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + DOMAIN, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE) from homeassistant.components.zwave import ZWaveDeviceEntity diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 7d69defed48..bbed1ffbbcd 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -218,7 +218,7 @@ def state_as_number(state: State) -> float: Raises ValueError if this is not possible. """ - from homeassistant.components.climate import ( + from homeassistant.components.climate.const import ( STATE_HEAT, STATE_COOL, STATE_IDLE) if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON, diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py index d1626e1f235..b5b6137a0a8 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -3,8 +3,9 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ -from homeassistant.components.climate import ( - _LOGGER, ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_FAN_MODE, ATTR_HOLD_MODE, +from homeassistant.components.climate import _LOGGER +from homeassistant.components.climate.const import ( + ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_FAN_MODE, ATTR_HOLD_MODE, ATTR_HUMIDITY, ATTR_OPERATION_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, SERVICE_SET_AWAY_MODE, SERVICE_SET_HOLD_MODE, SERVICE_SET_AUX_HEAT, SERVICE_SET_TEMPERATURE, SERVICE_SET_HUMIDITY, diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py index 3a023916741..3166b2d3158 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -8,7 +8,9 @@ from homeassistant.util.unit_system import ( METRIC_SYSTEM ) from homeassistant.setup import setup_component -from homeassistant.components import climate +from homeassistant.components.climate import ( + DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.const import (ATTR_ENTITY_ID) from tests.common import get_test_home_assistant from tests.components.climate import common @@ -26,7 +28,7 @@ class TestDemoClimate(unittest.TestCase): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.units = METRIC_SYSTEM - assert setup_component(self.hass, climate.DOMAIN, { + assert setup_component(self.hass, DOMAIN, { 'climate': { 'platform': 'demo', }}) @@ -267,14 +269,14 @@ class TestDemoClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_ECOBEE) assert 'auto' == state.state - self.hass.services.call(climate.DOMAIN, climate.SERVICE_TURN_OFF, - {climate.ATTR_ENTITY_ID: ENTITY_ECOBEE}) + self.hass.services.call(DOMAIN, SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ECOBEE}) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) assert 'off' == state.state - self.hass.services.call(climate.DOMAIN, climate.SERVICE_TURN_ON, - {climate.ATTR_ENTITY_ID: ENTITY_ECOBEE}) + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ECOBEE}) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) assert 'auto' == state.state diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index 8d2346260d9..1d532f4757c 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -20,8 +20,9 @@ from homeassistant.const import ( ) from homeassistant import loader from homeassistant.util.unit_system import METRIC_SYSTEM -from homeassistant.components import climate, input_boolean, switch -from homeassistant.components.climate import STATE_HEAT, STATE_COOL +from homeassistant.components import input_boolean, switch +from homeassistant.components.climate.const import ( + ATTR_OPERATION_MODE, STATE_HEAT, STATE_COOL, DOMAIN) import homeassistant.components as comps from tests.common import assert_setup_component, mock_restore_cache from tests.components.climate import common @@ -77,7 +78,7 @@ async def test_heater_input_boolean(hass, setup_comp_1): assert await async_setup_component(hass, input_boolean.DOMAIN, { 'input_boolean': {'test': None}}) - assert await async_setup_component(hass, climate.DOMAIN, {'climate': { + assert await async_setup_component(hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'heater': heater_switch, @@ -105,7 +106,7 @@ async def test_heater_switch(hass, setup_comp_1): 'platform': 'test'}}) heater_switch = switch_1.entity_id - assert await async_setup_component(hass, climate.DOMAIN, {'climate': { + assert await async_setup_component(hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'heater': heater_switch, @@ -134,7 +135,7 @@ def setup_comp_2(hass): """Initialize components.""" hass.config.units = METRIC_SYSTEM assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 2, @@ -162,7 +163,7 @@ async def test_get_operation_modes(hass, setup_comp_2): """Test that the operation list returns the correct modes.""" state = hass.states.get(ENTITY) modes = state.attributes.get('operation_list') - assert [climate.STATE_HEAT, STATE_OFF] == modes + assert [STATE_HEAT, STATE_OFF] == modes async def test_set_target_temp(hass, setup_comp_2): @@ -355,7 +356,7 @@ async def test_operating_mode_heat(hass, setup_comp_2): _setup_sensor(hass, 25) await hass.async_block_till_done() calls = _setup_switch(hass, False) - common.async_set_operation_mode(hass, climate.STATE_HEAT) + common.async_set_operation_mode(hass, STATE_HEAT) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -385,7 +386,7 @@ def setup_comp_3(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 2, @@ -433,7 +434,7 @@ async def test_operating_mode_cool(hass, setup_comp_3): _setup_sensor(hass, 30) await hass.async_block_till_done() calls = _setup_switch(hass, False) - common.async_set_operation_mode(hass, climate.STATE_COOL) + common.async_set_operation_mode(hass, STATE_COOL) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -535,7 +536,7 @@ def setup_comp_4(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 0.3, @@ -611,7 +612,7 @@ async def test_mode_change_ac_trigger_off_not_long_enough(hass, setup_comp_4): _setup_sensor(hass, 25) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, climate.STATE_OFF) + common.async_set_operation_mode(hass, STATE_OFF) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -628,7 +629,7 @@ async def test_mode_change_ac_trigger_on_not_long_enough(hass, setup_comp_4): _setup_sensor(hass, 30) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, climate.STATE_HEAT) + common.async_set_operation_mode(hass, STATE_HEAT) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -642,7 +643,7 @@ def setup_comp_5(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 0.3, @@ -720,7 +721,7 @@ async def test_mode_change_ac_trigger_off_not_long_enough_2( _setup_sensor(hass, 25) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, climate.STATE_OFF) + common.async_set_operation_mode(hass, STATE_OFF) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -737,7 +738,7 @@ async def test_mode_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5): _setup_sensor(hass, 30) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, climate.STATE_HEAT) + common.async_set_operation_mode(hass, STATE_HEAT) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -751,7 +752,7 @@ def setup_comp_6(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 0.3, @@ -829,7 +830,7 @@ async def test_mode_change_heater_trigger_off_not_long_enough( _setup_sensor(hass, 30) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, climate.STATE_OFF) + common.async_set_operation_mode(hass, STATE_OFF) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -847,7 +848,7 @@ async def test_mode_change_heater_trigger_on_not_long_enough( _setup_sensor(hass, 25) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, climate.STATE_HEAT) + common.async_set_operation_mode(hass, STATE_HEAT) await hass.async_block_till_done() assert 1 == len(calls) call = calls[0] @@ -861,7 +862,7 @@ def setup_comp_7(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 0.3, @@ -933,7 +934,7 @@ def setup_comp_8(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 0.3, @@ -1000,7 +1001,7 @@ def setup_comp_9(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_CELSIUS assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': [ + hass, DOMAIN, {'climate': [ { 'platform': 'generic_thermostat', 'name': 'test_heat', @@ -1080,7 +1081,7 @@ def setup_comp_10(hass): """Initialize components.""" hass.config.temperature_unit = TEMP_FAHRENHEIT assert hass.loop.run_until_complete(async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 0.3, @@ -1109,7 +1110,7 @@ async def test_precision(hass, setup_comp_10): async def test_custom_setup_params(hass): """Test the setup with custom parameters.""" result = await async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'heater': ENT_SWITCH, @@ -1129,13 +1130,14 @@ async def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache(hass, ( State('climate.test_thermostat', '0', {ATTR_TEMPERATURE: "20", - climate.ATTR_OPERATION_MODE: "off", ATTR_AWAY_MODE: "on"}), + ATTR_OPERATION_MODE: "off", + ATTR_AWAY_MODE: "on"}), )) hass.state = CoreState.starting await async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test_thermostat', 'heater': ENT_SWITCH, @@ -1144,7 +1146,7 @@ async def test_restore_state(hass): state = hass.states.get('climate.test_thermostat') assert(state.attributes[ATTR_TEMPERATURE] == 20) - assert(state.attributes[climate.ATTR_OPERATION_MODE] == "off") + assert(state.attributes[ATTR_OPERATION_MODE] == "off") assert(state.state == STATE_OFF) @@ -1155,13 +1157,14 @@ async def test_no_restore_state(hass): """ mock_restore_cache(hass, ( State('climate.test_thermostat', '0', {ATTR_TEMPERATURE: "20", - climate.ATTR_OPERATION_MODE: "off", ATTR_AWAY_MODE: "on"}), + ATTR_OPERATION_MODE: "off", + ATTR_AWAY_MODE: "on"}), )) hass.state = CoreState.starting await async_setup_component( - hass, climate.DOMAIN, {'climate': { + hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test_thermostat', 'heater': ENT_SWITCH, @@ -1191,7 +1194,7 @@ async def test_restore_state_uncoherence_case(hass): state = hass.states.get(ENTITY) assert 20 == state.attributes[ATTR_TEMPERATURE] assert STATE_OFF == \ - state.attributes[climate.ATTR_OPERATION_MODE] + state.attributes[ATTR_OPERATION_MODE] assert STATE_OFF == state.state assert 0 == len(calls) @@ -1199,12 +1202,12 @@ async def test_restore_state_uncoherence_case(hass): await hass.async_block_till_done() state = hass.states.get(ENTITY) assert STATE_OFF == \ - state.attributes[climate.ATTR_OPERATION_MODE] + state.attributes[ATTR_OPERATION_MODE] assert STATE_OFF == state.state async def _setup_climate(hass): - assert await async_setup_component(hass, climate.DOMAIN, {'climate': { + assert await async_setup_component(hass, DOMAIN, {'climate': { 'platform': 'generic_thermostat', 'name': 'test', 'cold_tolerance': 2, @@ -1220,6 +1223,6 @@ def _mock_restore_cache(hass, temperature=20, operation_mode=STATE_OFF): mock_restore_cache(hass, ( State(ENTITY, '0', { ATTR_TEMPERATURE: str(temperature), - climate.ATTR_OPERATION_MODE: operation_mode, + ATTR_OPERATION_MODE: operation_mode, ATTR_AWAY_MODE: "on"}), )) diff --git a/tests/components/climate/test_honeywell.py b/tests/components/climate/test_honeywell.py index 7daed2ff4a9..b01b5b35c35 100644 --- a/tests/components/climate/test_honeywell.py +++ b/tests/components/climate/test_honeywell.py @@ -8,7 +8,7 @@ import somecomfort from homeassistant.const import ( CONF_USERNAME, CONF_PASSWORD, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( ATTR_FAN_MODE, ATTR_OPERATION_MODE, ATTR_FAN_LIST, ATTR_OPERATION_LIST) import homeassistant.components.climate.honeywell as honeywell diff --git a/tests/components/climate/test_melissa.py b/tests/components/climate/test_melissa.py index 2c135bfc09d..6b77981a914 100644 --- a/tests/components/climate/test_melissa.py +++ b/tests/components/climate/test_melissa.py @@ -4,8 +4,9 @@ import json from homeassistant.components.climate.melissa import MelissaClimate -from homeassistant.components.climate import ( - melissa, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, +from homeassistant.components.climate import melissa +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_ON_OFF, SUPPORT_FAN_MODE, STATE_HEAT, STATE_FAN_ONLY, STATE_DRY, STATE_COOL, STATE_AUTO ) diff --git a/tests/components/climate/test_nuheat.py b/tests/components/climate/test_nuheat.py index 40b0732f661..19919d25954 100644 --- a/tests/components/climate/test_nuheat.py +++ b/tests/components/climate/test_nuheat.py @@ -3,7 +3,7 @@ import unittest from unittest.mock import Mock, patch from tests.common import get_test_home_assistant -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py index c16151320b4..8ec8e7b1429 100644 --- a/tests/components/climate/test_reproduce_state.py +++ b/tests/components/climate/test_reproduce_state.py @@ -2,13 +2,13 @@ import pytest -from homeassistant.components.climate import STATE_HEAT, async_reproduce_states +from homeassistant.components.climate import async_reproduce_states from homeassistant.components.climate.const import ( ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_HOLD_MODE, ATTR_HUMIDITY, ATTR_OPERATION_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, SERVICE_SET_AUX_HEAT, SERVICE_SET_AWAY_MODE, SERVICE_SET_HOLD_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_OPERATION_MODE, - SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE) + SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, STATE_HEAT) from homeassistant.const import ( ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON) from homeassistant.core import Context, State diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 965fb37dcb8..f4412b5d564 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -3,7 +3,7 @@ import unittest from unittest import mock import homeassistant.const as const from homeassistant.components.ecobee import climate as ecobee -from homeassistant.components.climate import STATE_OFF +from homeassistant.const import STATE_OFF class TestEcobee(unittest.TestCase): diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 89e9090da98..18f99c82685 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -8,7 +8,8 @@ import pytest from homeassistant import core, const, setup from homeassistant.components import ( - fan, cover, light, switch, climate, lock, async_setup, media_player) + fan, cover, light, switch, lock, async_setup, media_player) +from homeassistant.components.climate import const as climate from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.components import google_assistant as ga diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 36971224f92..d7c6094bebb 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -3,7 +3,9 @@ from homeassistant.core import State from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from homeassistant.setup import async_setup_component -from homeassistant.components import climate +from homeassistant.components.climate.const import ( + ATTR_MIN_TEMP, ATTR_MAX_TEMP, STATE_HEAT, SUPPORT_OPERATION_MODE +) from homeassistant.components.google_assistant import ( const, trait, helpers, smart_home as sh) from homeassistant.components.light.demo import DemoLight @@ -210,10 +212,10 @@ async def test_execute(hass): async def test_raising_error_trait(hass): """Test raising an error while executing a trait command.""" - hass.states.async_set('climate.bla', climate.STATE_HEAT, { - climate.ATTR_MIN_TEMP: 15, - climate.ATTR_MAX_TEMP: 30, - ATTR_SUPPORTED_FEATURES: climate.SUPPORT_OPERATION_MODE, + hass.states.async_set('climate.bla', STATE_HEAT, { + ATTR_MIN_TEMP: 15, + ATTR_MAX_TEMP: 30, + ATTR_SUPPORTED_FEATURES: SUPPORT_OPERATION_MODE, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }) result = await sh.async_handle_message(hass, BASIC_CONFIG, { diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index e9169c9bbbe..e051a5de4da 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -2,7 +2,6 @@ import pytest from homeassistant.components import ( - climate, cover, fan, input_boolean, @@ -15,10 +14,11 @@ from homeassistant.components import ( vacuum, group, ) +from homeassistant.components.climate import const as climate from homeassistant.components.google_assistant import trait, helpers, const from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES) + TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE) from homeassistant.core import State, DOMAIN as HA_DOMAIN from homeassistant.util import color from tests.common import async_mock_service @@ -668,7 +668,7 @@ async def test_temperature_setting_climate_range(hass): climate.ATTR_CURRENT_HUMIDITY: 25, climate.ATTR_OPERATION_MODE: climate.STATE_AUTO, climate.ATTR_OPERATION_LIST: [ - climate.STATE_OFF, + STATE_OFF, climate.STATE_COOL, climate.STATE_HEAT, climate.STATE_AUTO, @@ -737,12 +737,12 @@ async def test_temperature_setting_climate_setpoint(hass): 'climate.bla', climate.STATE_AUTO, { climate.ATTR_OPERATION_MODE: climate.STATE_COOL, climate.ATTR_OPERATION_LIST: [ - climate.STATE_OFF, + STATE_OFF, climate.STATE_COOL, ], climate.ATTR_MIN_TEMP: 10, climate.ATTR_MAX_TEMP: 30, - climate.ATTR_TEMPERATURE: 18, + ATTR_TEMPERATURE: 18, climate.ATTR_CURRENT_TEMPERATURE: 20 }), BASIC_CONFIG) assert trt.sync_attributes() == { @@ -772,7 +772,7 @@ async def test_temperature_setting_climate_setpoint(hass): assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'climate.bla', - climate.ATTR_TEMPERATURE: 19 + ATTR_TEMPERATURE: 19 } diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index f645cddf730..ce6774796d3 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 9f5cc9d8764..b04a57fa967 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -1,5 +1,5 @@ """Basic checks for HomeKitclimate.""" -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( DOMAIN, SERVICE_SET_OPERATION_MODE, SERVICE_SET_TEMPERATURE) from tests.components.homekit_controller.common import ( setup_test_component) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index ecbdc39e22b..7bdfe8f452f 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -7,11 +7,15 @@ from unittest.mock import ANY import pytest import voluptuous as vol -from homeassistant.components import climate, mqtt +from homeassistant.components import mqtt from homeassistant.components.climate import ( - DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, + DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP) +from homeassistant.components.climate.const import ( + DOMAIN as CLIMATE_DOMAIN, + SUPPORT_AUX_HEAT, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE) + SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, STATE_AUTO, + STATE_COOL, STATE_HEAT, STATE_DRY, STATE_FAN_ONLY) from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.setup import setup_component @@ -54,7 +58,7 @@ class TestMQTTClimate(unittest.TestCase): def test_setup_params(self): """Test the initial parameters.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert 21 == state.attributes.get('temperature') @@ -66,7 +70,7 @@ class TestMQTTClimate(unittest.TestCase): def test_supported_features(self): """Test the supported_features.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) support = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | @@ -77,13 +81,13 @@ class TestMQTTClimate(unittest.TestCase): def test_get_operation_modes(self): """Test that the operation list returns the correct modes.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) modes = state.attributes.get('operation_list') assert [ - climate.STATE_AUTO, STATE_OFF, climate.STATE_COOL, - climate.STATE_HEAT, climate.STATE_DRY, climate.STATE_FAN_ONLY + STATE_AUTO, STATE_OFF, STATE_COOL, + STATE_HEAT, STATE_DRY, STATE_FAN_ONLY ] == modes def test_set_operation_bad_attr_and_state(self): @@ -91,7 +95,7 @@ class TestMQTTClimate(unittest.TestCase): Also check the state. """ - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert "off" == state.attributes.get('operation_mode') @@ -105,7 +109,7 @@ class TestMQTTClimate(unittest.TestCase): def test_set_operation(self): """Test setting of new operation mode.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert "off" == state.attributes.get('operation_mode') @@ -122,7 +126,7 @@ class TestMQTTClimate(unittest.TestCase): """Test setting operation mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['mode_state_topic'] = 'mode-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') is None @@ -150,7 +154,7 @@ class TestMQTTClimate(unittest.TestCase): """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) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert "off" == state.attributes.get('operation_mode') @@ -179,7 +183,7 @@ class TestMQTTClimate(unittest.TestCase): def test_set_fan_mode_bad_attr(self): """Test setting fan mode without required attribute.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert "low" == state.attributes.get('fan_mode') @@ -193,7 +197,7 @@ class TestMQTTClimate(unittest.TestCase): """Test setting of new fan mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['fan_mode_state_topic'] = 'fan-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('fan_mode') is None @@ -215,7 +219,7 @@ class TestMQTTClimate(unittest.TestCase): def test_set_fan_mode(self): """Test setting of new fan mode.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert "low" == state.attributes.get('fan_mode') @@ -228,7 +232,7 @@ class TestMQTTClimate(unittest.TestCase): def test_set_swing_mode_bad_attr(self): """Test setting swing mode without required attribute.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert "off" == state.attributes.get('swing_mode') @@ -242,7 +246,7 @@ class TestMQTTClimate(unittest.TestCase): """Test setting swing mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['swing_mode_state_topic'] = 'swing-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('swing_mode') is None @@ -264,7 +268,7 @@ class TestMQTTClimate(unittest.TestCase): def test_set_swing(self): """Test setting of new swing mode.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert "off" == state.attributes.get('swing_mode') @@ -277,7 +281,7 @@ class TestMQTTClimate(unittest.TestCase): def test_set_target_temperature(self): """Test setting the target temperature.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert 21 == state.attributes.get('temperature') @@ -315,7 +319,7 @@ class TestMQTTClimate(unittest.TestCase): """Test setting the target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['temperature_state_topic'] = 'temperature-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('temperature') is None @@ -342,7 +346,7 @@ class TestMQTTClimate(unittest.TestCase): config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['current_temperature_topic'] = 'current_temperature' mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) fire_mqtt_message(self.hass, 'current_temperature', '47') self.hass.block_till_done() @@ -353,7 +357,7 @@ class TestMQTTClimate(unittest.TestCase): """Test setting of the away mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['away_mode_state_topic'] = 'away-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert 'off' == state.attributes.get('away_mode') @@ -384,7 +388,7 @@ class TestMQTTClimate(unittest.TestCase): config['climate']['payload_on'] = 'AN' config['climate']['payload_off'] = 'AUS' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert 'off' == state.attributes.get('away_mode') @@ -407,7 +411,7 @@ class TestMQTTClimate(unittest.TestCase): """Test setting the hold mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['hold_state_topic'] = 'hold-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('hold_mode') is None @@ -429,7 +433,7 @@ class TestMQTTClimate(unittest.TestCase): def test_set_hold(self): """Test setting the hold mode.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('hold_mode') is None @@ -452,7 +456,7 @@ class TestMQTTClimate(unittest.TestCase): """Test setting of the aux heating in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['aux_state_topic'] = 'aux-state' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) assert 'off' == state.attributes.get('aux_heat') @@ -479,7 +483,7 @@ class TestMQTTClimate(unittest.TestCase): def test_set_aux(self): """Test setting of the aux heating.""" - assert setup_component(self.hass, climate.DOMAIN, DEFAULT_CONFIG) + assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = self.hass.states.get(ENTITY_CLIMATE) assert 'off' == state.attributes.get('aux_heat') @@ -505,7 +509,7 @@ class TestMQTTClimate(unittest.TestCase): config['climate']['payload_available'] = 'good' config['climate']['payload_not_available'] = 'nogood' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get('climate.test') assert STATE_UNAVAILABLE == state.state @@ -543,7 +547,7 @@ class TestMQTTClimate(unittest.TestCase): config['climate']['aux_state_topic'] = 'aux-state' config['climate']['current_temperature_topic'] = 'current-temperature' - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) # Operation Mode state = self.hass.states.get(ENTITY_CLIMATE) @@ -638,7 +642,7 @@ class TestMQTTClimate(unittest.TestCase): config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['min_temp'] = 26 - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) min_temp = state.attributes.get('min_temp') @@ -651,7 +655,7 @@ class TestMQTTClimate(unittest.TestCase): config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['max_temp'] = 60 - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) max_temp = state.attributes.get('max_temp') @@ -664,7 +668,7 @@ class TestMQTTClimate(unittest.TestCase): config = copy.deepcopy(DEFAULT_CONFIG) config['climate']['temp_step'] = 0.01 - assert setup_component(self.hass, climate.DOMAIN, config) + assert setup_component(self.hass, CLIMATE_DOMAIN, config) state = self.hass.states.get(ENTITY_CLIMATE) temp_step = state.attributes.get('target_temp_step') @@ -675,8 +679,8 @@ class TestMQTTClimate(unittest.TestCase): async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" - assert await async_setup_component(hass, climate.DOMAIN, { - climate.DOMAIN: { + assert await async_setup_component(hass, CLIMATE_DOMAIN, { + CLIMATE_DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'power_state_topic': 'test-topic', @@ -694,8 +698,8 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): """Test attributes get extracted from a JSON result.""" - assert await async_setup_component(hass, climate.DOMAIN, { - climate.DOMAIN: { + assert await async_setup_component(hass, CLIMATE_DOMAIN, { + CLIMATE_DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'power_state_topic': 'test-topic', @@ -714,8 +718,8 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): """Test attributes get extracted from a JSON result.""" - assert await async_setup_component(hass, climate.DOMAIN, { - climate.DOMAIN: { + assert await async_setup_component(hass, CLIMATE_DOMAIN, { + CLIMATE_DOMAIN: { 'platform': 'mqtt', 'name': 'test', 'power_state_topic': 'test-topic', @@ -781,8 +785,8 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async def test_unique_id(hass): """Test unique id option only creates one climate per unique_id.""" await async_mock_mqtt_component(hass) - assert await async_setup_component(hass, climate.DOMAIN, { - climate.DOMAIN: [{ + assert await async_setup_component(hass, CLIMATE_DOMAIN, { + CLIMATE_DOMAIN: [{ 'platform': 'mqtt', 'name': 'Test 1', 'power_state_topic': 'test-topic', @@ -798,7 +802,7 @@ async def test_unique_id(hass): }) async_fire_mqtt_message(hass, 'test-topic', 'payload') await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(climate.DOMAIN)) == 1 + assert len(hass.states.async_entity_ids(CLIMATE_DOMAIN)) == 1 async def test_discovery_removal_climate(hass, mqtt_mock, caplog): @@ -974,8 +978,8 @@ async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" registry = mock_registry(hass, {}) mock_mqtt = await async_mock_mqtt_component(hass) - assert await async_setup_component(hass, climate.DOMAIN, { - climate.DOMAIN: [{ + assert await async_setup_component(hass, CLIMATE_DOMAIN, { + CLIMATE_DOMAIN: [{ 'platform': 'mqtt', 'name': 'beer', 'mode_state_topic': 'test-topic', diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 0f1102e2ab1..c615ca66e34 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -8,18 +8,18 @@ from pysmartthings import Attribute, Capability from pysmartthings.device import Status import pytest -from homeassistant.components.climate import ( +from homeassistant.components.climate.const import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_LIST, ATTR_FAN_MODE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, SERVICE_SET_OPERATION_MODE, SERVICE_SET_TEMPERATURE, - STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, STATE_OFF, SUPPORT_FAN_MODE, + STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.smartthings import climate from homeassistant.components.smartthings.const import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, STATE_OFF) from .conftest import setup_platform diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index 9a9ed41381f..b5e5639bdc6 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -1,7 +1,7 @@ """Test Z-Wave climate devices.""" import pytest -from homeassistant.components.climate import STATE_COOL, STATE_HEAT +from homeassistant.components.climate.const import STATE_COOL, STATE_HEAT from homeassistant.components.zwave import climate from homeassistant.const import ( STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) From 9c09a98c9e636393608e20c2599ada68da80a3c2 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 14 Feb 2019 20:55:51 +0100 Subject: [PATCH 006/253] Add legacy PLATFORM_SCHEMA config validation --- homeassistant/helpers/config_validation.py | 51 +++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 3b01a01fc96..473fcc88717 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -25,6 +25,8 @@ from homeassistant.helpers import template as template_helper from homeassistant.helpers.logging import KeywordStyleAdapter from homeassistant.util import slugify as util_slugify +_LOGGER = logging.getLogger(__name__) + # pylint: disable=invalid-name TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM' or 'HH:MM:SS'" @@ -632,8 +634,55 @@ def key_dependency(key, dependency): # Schemas +class HASchema(vol.Schema): + """Schema class that allows us to mark PREVENT_EXTRA errors as warnings.""" + def __call__(self, data): + try: + return super().__call__(data) + except vol.Invalid as orig_err: + if self.extra != vol.PREVENT_EXTRA: + raise -PLATFORM_SCHEMA = vol.Schema({ + # orig_error is of type vol.MultipleInvalid (see super __call__) + assert isinstance(orig_err, vol.MultipleInvalid) + # If it fails with PREVENT_EXTRA, try with ALLOW_EXTRA + self.extra = vol.ALLOW_EXTRA + # In case it still fails the following will raise + try: + validated = super().__call__(data) + finally: + self.extra = vol.PREVENT_EXTRA + + # This is a legacy config, print warning + extra_key_errs = [err for err in orig_err.errors + if err.error_message == 'extra keys not allowed'] + if extra_key_errs: + msg = "Your configuration contains extra keys " \ + "that the platform does not support. The keys " + msg += ', '.join('[{}]'.format(err.path[-1]) for err in + extra_key_errs) + msg += ' are 42.' + if hasattr(data, '__config_file__'): + msg += " (See {}, line {}). ".format(data.__config_file__, + data.__line__) + _LOGGER.warning(msg) + else: + # This should not happen (all errors should be extra key + # errors). Let's raise the original error anyway. + raise orig_err + + # Return legacy validated config + return validated + + def extend(self, schema, required=None, extra=None): + """Extend this schema and convert it to HASchema if necessary""" + ret = super().extend(schema, required=required, extra=extra) + if extra is not None: + return ret + return HASchema(ret.schema, required=required, extra=self.extra) + + +PLATFORM_SCHEMA = HASchema({ vol.Required(CONF_PLATFORM): string, vol.Optional(CONF_ENTITY_NAMESPACE): string, vol.Optional(CONF_SCAN_INTERVAL): time_period From d60934c028b6517e4ec564df4dc5b08cc7e7f5c3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Feb 2019 12:06:25 -0800 Subject: [PATCH 007/253] Fix pushover schema --- homeassistant/components/notify/pushover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index 3ec0b27e7c4..b249ca804b3 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_TARGET, ATTR_DATA, - BaseNotificationService) + BaseNotificationService, PLATFORM_SCHEMA) from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) CONF_USER_KEY = 'user_key' -PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USER_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string, }) From cdc4dc3f11f60792959b17a6e227d1f71c72adf4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 14 Feb 2019 22:09:22 +0100 Subject: [PATCH 008/253] Rename CONF_ATTRIBUTION to ATTRIBUTION (#21069) * Rename CONF_ATTRIBUTION to ATTRIBUTION * Update homeassistant/components/sensor/irish_rail_transport.py Co-Authored-By: fabaff --- homeassistant/components/abode/__init__.py | 7 +-- .../components/abode/alarm_control_panel.py | 4 +- homeassistant/components/arlo/__init__.py | 2 +- .../components/arlo/alarm_control_panel.py | 4 +- homeassistant/components/arlo/sensor.py | 4 +- .../components/binary_sensor/aurora.py | 6 +-- .../components/binary_sensor/ring.py | 18 +++----- .../components/binary_sensor/uptimerobot.py | 4 +- homeassistant/components/camera/ring.py | 7 ++- .../components/digital_ocean/__init__.py | 3 +- .../components/digital_ocean/binary_sensor.py | 4 +- .../components/digital_ocean/switch.py | 4 +- .../components/hydrawise/__init__.py | 5 ++- .../components/logi_circle/__init__.py | 8 ++-- .../components/logi_circle/camera.py | 4 +- .../components/logi_circle/sensor.py | 4 +- .../components/raincloud/__init__.py | 5 ++- homeassistant/components/raincloud/switch.py | 16 ++----- homeassistant/components/ring/__init__.py | 6 +-- .../components/sensor/alpha_vantage.py | 7 +-- homeassistant/components/sensor/bitcoin.py | 4 +- homeassistant/components/sensor/blockchain.py | 5 ++- homeassistant/components/sensor/bom.py | 5 ++- homeassistant/components/sensor/coinbase.py | 7 +-- .../components/sensor/coinmarketcap.py | 5 ++- .../components/sensor/comed_hourly_pricing.py | 6 +-- .../components/sensor/currencylayer.py | 4 +- homeassistant/components/sensor/discogs.py | 4 +- .../components/sensor/dublin_bus_transport.py | 5 ++- .../sensor/entur_public_transport.py | 5 ++- homeassistant/components/sensor/etherscan.py | 7 ++- homeassistant/components/sensor/fitbit.py | 8 ++-- homeassistant/components/sensor/fixer.py | 4 +- homeassistant/components/sensor/gitlab_ci.py | 4 +- .../components/sensor/haveibeenpwned.py | 6 ++- homeassistant/components/sensor/iperf3.py | 10 ++--- .../components/sensor/irish_rail_transport.py | 43 +++++++++---------- homeassistant/components/sensor/lastfm.py | 11 ++--- .../components/sensor/london_underground.py | 6 ++- homeassistant/components/sensor/metoffice.py | 4 +- .../sensor/nederlandse_spoorwegen.py | 5 ++- .../components/sensor/nsw_fuel_station.py | 5 ++- .../components/sensor/openexchangerates.py | 4 +- .../components/sensor/openweathermap.py | 5 ++- .../components/sensor/rejseplanen.py | 31 ++++++------- homeassistant/components/sensor/ring.py | 4 +- homeassistant/components/sensor/ripple.py | 4 +- homeassistant/components/sensor/sochain.py | 9 ++-- .../sensor/swiss_public_transport.py | 5 ++- .../components/sensor/synologydsm.py | 12 ++---- homeassistant/components/sensor/sytadin.py | 5 +-- .../sensor/trafikverket_weatherstation.py | 13 +++--- .../components/sensor/transport_nsw.py | 14 +++--- homeassistant/components/sensor/travisci.py | 5 ++- homeassistant/components/sensor/vasttrafik.py | 4 +- .../components/sensor/viaggiatreno.py | 7 +-- .../components/sensor/waze_travel_time.py | 5 ++- .../components/sensor/worldtidesinfo.py | 4 +- homeassistant/components/sensor/wsdot.py | 4 +- .../components/sensor/wunderground.py | 7 ++- homeassistant/components/sensor/yr.py | 6 +-- homeassistant/components/sensor/yweather.py | 5 ++- homeassistant/components/sensor/zestimate.py | 5 ++- homeassistant/components/skybell/__init__.py | 4 +- homeassistant/components/weather/met.py | 20 ++++----- homeassistant/components/weather/metoffice.py | 4 +- 66 files changed, 232 insertions(+), 239 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 71a1dcdd590..591bae1a9cf 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -17,7 +17,8 @@ REQUIREMENTS = ['abodepy==0.15.0'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by goabode.com" +ATTRIBUTION = "Data provided by goabode.com" + CONF_POLLING = 'polling' DOMAIN = 'abode' @@ -280,7 +281,7 @@ class AbodeDevice(Entity): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'device_id': self._device.device_id, 'battery_low': self._device.battery_low, 'no_response': self._device.no_response, @@ -327,7 +328,7 @@ class AbodeAutomation(Entity): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'automation_id': self._automation.automation_id, 'type': self._automation.type, 'sub_type': self._automation.sub_type diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index ec5038a7a84..838d09b73af 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -2,7 +2,7 @@ import logging import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.abode import CONF_ATTRIBUTION, AbodeDevice +from homeassistant.components.abode import ATTRIBUTION, AbodeDevice from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -73,7 +73,7 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'device_id': self._device.device_id, 'battery_backup': self._device.battery, 'cellular_backup': self._device.is_cellular, diff --git a/homeassistant/components/arlo/__init__.py b/homeassistant/components/arlo/__init__.py index 7e81836e522..cbb720778e5 100644 --- a/homeassistant/components/arlo/__init__.py +++ b/homeassistant/components/arlo/__init__.py @@ -15,7 +15,7 @@ REQUIREMENTS = ['pyarlo==0.2.3'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by arlo.netgear.com" +ATTRIBUTION = "Data provided by arlo.netgear.com" DATA_ARLO = 'data_arlo' DEFAULT_BRAND = 'Netgear Arlo' diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py index 8c21a448a23..931dfa1b15d 100644 --- a/homeassistant/components/arlo/alarm_control_panel.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -9,7 +9,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.alarm_control_panel import ( AlarmControlPanel, PLATFORM_SCHEMA) from homeassistant.components.arlo import ( - DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO) + DATA_ARLO, ATTRIBUTION, SIGNAL_UPDATE_ARLO) from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT) @@ -117,7 +117,7 @@ class ArloBaseStation(AlarmControlPanel): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'device_id': self._base_station.device_id } diff --git a/homeassistant/components/arlo/sensor.py b/homeassistant/components/arlo/sensor.py index 3ad7b70a947..1c3cc933438 100644 --- a/homeassistant/components/arlo/sensor.py +++ b/homeassistant/components/arlo/sensor.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.components.arlo import ( - CONF_ATTRIBUTION, DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO) + ATTRIBUTION, DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO) from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, @@ -177,7 +177,7 @@ class ArloSensor(Entity): """Return the device state attributes.""" attrs = {} - attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION attrs['brand'] = DEFAULT_BRAND if self._sensor_type != 'total_cameras': diff --git a/homeassistant/components/binary_sensor/aurora.py b/homeassistant/components/binary_sensor/aurora.py index 04b402722b2..cfd683346ff 100644 --- a/homeassistant/components/binary_sensor/aurora.py +++ b/homeassistant/components/binary_sensor/aurora.py @@ -19,8 +19,8 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric" \ - "Administration" +ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric " \ + "Administration" CONF_THRESHOLD = 'forecast_threshold' DEFAULT_DEVICE_CLASS = 'visible' @@ -91,7 +91,7 @@ class AuroraSensor(BinarySensorDevice): if self.aurora_data: attrs['visibility_level'] = self.aurora_data.visibility_level attrs['message'] = self.aurora_data.is_visible_text - attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION return attrs def update(self): diff --git a/homeassistant/components/binary_sensor/ring.py b/homeassistant/components/binary_sensor/ring.py index 5a65917f40b..79fc61a62d4 100644 --- a/homeassistant/components/binary_sensor/ring.py +++ b/homeassistant/components/binary_sensor/ring.py @@ -11,7 +11,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.ring import ( - CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DATA_RING) + ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DATA_RING) from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS) @@ -47,18 +47,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for device in ring.doorbells: # ring.doorbells is doing I/O for sensor_type in config[CONF_MONITORED_CONDITIONS]: if 'doorbell' in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingBinarySensor(hass, - device, - sensor_type)) + sensors.append(RingBinarySensor(hass, device, sensor_type)) for device in ring.stickup_cams: # ring.stickup_cams is doing I/O for sensor_type in config[CONF_MONITORED_CONDITIONS]: if 'stickup_cams' in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingBinarySensor(hass, - device, - sensor_type)) + sensors.append(RingBinarySensor(hass, device, sensor_type)) + add_entities(sensors, True) - return True class RingBinarySensor(BinarySensorDevice): @@ -69,8 +65,8 @@ class RingBinarySensor(BinarySensorDevice): super(RingBinarySensor, self).__init__() self._sensor_type = sensor_type self._data = data - self._name = "{0} {1}".format(self._data.name, - SENSOR_TYPES.get(self._sensor_type)[0]) + self._name = "{0} {1}".format( + self._data.name, SENSOR_TYPES.get(self._sensor_type)[0]) self._device_class = SENSOR_TYPES.get(self._sensor_type)[2] self._state = None self._unique_id = '{}-{}'.format(self._data.id, self._sensor_type) @@ -99,7 +95,7 @@ class RingBinarySensor(BinarySensorDevice): def device_state_attributes(self): """Return the state attributes.""" attrs = {} - attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION attrs['device_id'] = self._data.id attrs['firmware'] = self._data.firmware diff --git a/homeassistant/components/binary_sensor/uptimerobot.py b/homeassistant/components/binary_sensor/uptimerobot.py index dbb83e53e9f..e48ac3039ae 100644 --- a/homeassistant/components/binary_sensor/uptimerobot.py +++ b/homeassistant/components/binary_sensor/uptimerobot.py @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_TARGET = 'target' -CONF_ATTRIBUTION = "Data provided by Uptime Robot" +ATTRIBUTION = "Data provided by Uptime Robot" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, @@ -78,7 +78,7 @@ class UptimeRobotBinarySensor(BinarySensorDevice): def device_state_attributes(self): """Return the state attributes of the binary sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_TARGET: self._target, } diff --git a/homeassistant/components/camera/ring.py b/homeassistant/components/camera/ring.py index da1119281b3..ce9ceb7b76f 100644 --- a/homeassistant/components/camera/ring.py +++ b/homeassistant/components/camera/ring.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.helpers import config_validation as cv from homeassistant.components.ring import ( - DATA_RING, CONF_ATTRIBUTION, NOTIFICATION_ID) + DATA_RING, ATTRIBUTION, NOTIFICATION_ID) from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_ATTRIBUTION, CONF_SCAN_INTERVAL @@ -34,8 +34,7 @@ SCAN_INTERVAL = timedelta(seconds=90) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): - cv.time_period, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, }) @@ -106,7 +105,7 @@ class RingCam(Camera): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'device_id': self._camera.id, 'firmware': self._camera.firmware, 'kind': self._camera.kind, diff --git a/homeassistant/components/digital_ocean/__init__.py b/homeassistant/components/digital_ocean/__init__.py index d061dad6726..7975a6eea0d 100644 --- a/homeassistant/components/digital_ocean/__init__.py +++ b/homeassistant/components/digital_ocean/__init__.py @@ -22,7 +22,8 @@ ATTR_MEMORY = 'memory' ATTR_REGION = 'region' ATTR_VCPUS = 'vcpus' -CONF_ATTRIBUTION = 'Data provided by Digital Ocean' +ATTRIBUTION = 'Data provided by Digital Ocean' + CONF_DROPLETS = 'droplets' DATA_DIGITAL_OCEAN = 'data_do' diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index 255f43b67ba..88df56cc629 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -9,7 +9,7 @@ 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, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) + ATTR_REGION, ATTR_VCPUS, ATTRIBUTION, DATA_DIGITAL_OCEAN) from homeassistant.const import ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -71,7 +71,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice): def device_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: 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/switch.py b/homeassistant/components/digital_ocean/switch.py index a10c961b8e4..9b5ddda3408 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -8,7 +8,7 @@ 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, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN) + ATTR_REGION, ATTR_VCPUS, ATTRIBUTION, DATA_DIGITAL_OCEAN) from homeassistant.const import ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -65,7 +65,7 @@ class DigitalOceanSwitch(SwitchDevice): def device_state_attributes(self): """Return the state attributes of the Digital Ocean droplet.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: 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/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index 800d19d7efe..9c7baf6db2e 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -20,7 +20,8 @@ _LOGGER = logging.getLogger(__name__) ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60] -CONF_ATTRIBUTION = "Data provided by hydrawise.com" +ATTRIBUTION = "Data provided by hydrawise.com" + CONF_WATERING_TIME = 'watering_minutes' NOTIFICATION_ID = 'hydrawise_notification' @@ -141,6 +142,6 @@ class HydrawiseEntity(Entity): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'identifier': self.data.get('relay'), } diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 50500f47e42..ef006ef8b4d 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -1,19 +1,19 @@ """Support for Logi Circle devices.""" -import logging import asyncio +import logging -import voluptuous as vol import async_timeout +import voluptuous as vol +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD REQUIREMENTS = ['logi_circle==0.1.7'] _LOGGER = logging.getLogger(__name__) _TIMEOUT = 15 # seconds -CONF_ATTRIBUTION = "Data provided by circle.logi.com" +ATTRIBUTION = "Data provided by circle.logi.com" NOTIFICATION_ID = 'logi_notification' NOTIFICATION_TITLE = 'Logi Circle Setup' diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 51bd7c124a3..4f349dd986e 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.helpers import config_validation as cv from homeassistant.components.logi_circle import ( - DOMAIN as LOGI_CIRCLE_DOMAIN, CONF_ATTRIBUTION) + DOMAIN as LOGI_CIRCLE_DOMAIN, ATTRIBUTION) from homeassistant.components.camera import ( Camera, PLATFORM_SCHEMA, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, ATTR_ENTITY_ID, ATTR_FILENAME, DOMAIN) @@ -128,7 +128,7 @@ class LogiCam(Camera): def device_state_attributes(self): """Return the state attributes.""" state = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'battery_saving_mode': ( STATE_ON if self._camera.battery_saving else STATE_OFF), 'ip_address': self._camera.ip_address, diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index 74c2039c120..4830219091c 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -5,7 +5,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.logi_circle import ( - CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DOMAIN as LOGI_CIRCLE_DOMAIN) + ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DOMAIN as LOGI_CIRCLE_DOMAIN) from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_BATTERY_CHARGING, @@ -86,7 +86,7 @@ class LogiSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" state = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'battery_saving_mode': ( STATE_ON if self._camera.battery_saving else STATE_OFF), 'ip_address': self._camera.ip_address, diff --git a/homeassistant/components/raincloud/__init__.py b/homeassistant/components/raincloud/__init__.py index 47f6176d5f8..7ccf9f33ada 100644 --- a/homeassistant/components/raincloud/__init__.py +++ b/homeassistant/components/raincloud/__init__.py @@ -24,7 +24,8 @@ _LOGGER = logging.getLogger(__name__) ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60] -CONF_ATTRIBUTION = "Data provided by Melnor Aquatimer.com" +ATTRIBUTION = "Data provided by Melnor Aquatimer.com" + CONF_WATERING_TIME = 'watering_minutes' NOTIFICATION_ID = 'raincloud_notification' @@ -165,7 +166,7 @@ class RainCloudEntity(Entity): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'identifier': self.data.serial, } diff --git a/homeassistant/components/raincloud/switch.py b/homeassistant/components/raincloud/switch.py index 969169edc64..1b76e8974b0 100644 --- a/homeassistant/components/raincloud/switch.py +++ b/homeassistant/components/raincloud/switch.py @@ -1,16 +1,11 @@ -""" -Support for Melnor RainCloud sprinkler water timer. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.raincloud/ -""" +"""Support for Melnor RainCloud sprinkler water timer.""" import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.raincloud import ( - ALLOWED_WATERING_TIME, CONF_ATTRIBUTION, CONF_WATERING_TIME, + ALLOWED_WATERING_TIME, ATTRIBUTION, CONF_WATERING_TIME, DATA_RAINCLOUD, DEFAULT_WATERING_TIME, RainCloudEntity, SWITCHES) from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA from homeassistant.const import ( @@ -38,12 +33,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # create a sensor for each zone managed by faucet for zone in raincloud.controller.faucet.zones: sensors.append( - RainCloudSwitch(default_watering_timer, - zone, - sensor_type)) + RainCloudSwitch(default_watering_timer, zone, sensor_type)) add_entities(sensors, True) - return True class RainCloudSwitch(RainCloudEntity, SwitchDevice): @@ -87,7 +79,7 @@ class RainCloudSwitch(RainCloudEntity, SwitchDevice): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'default_manual_timer': self._default_watering_timer, 'identifier': self.data.serial } diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 526388a0918..94f3be305fa 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -1,17 +1,17 @@ """Support for Ring Doorbell/Chimes.""" import logging -from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD REQUIREMENTS = ['ring_doorbell==0.2.2'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by Ring.com" +ATTRIBUTION = "Data provided by Ring.com" NOTIFICATION_ID = 'ring_notification' NOTIFICATION_TITLE = 'Ring Setup' diff --git a/homeassistant/components/sensor/alpha_vantage.py b/homeassistant/components/sensor/alpha_vantage.py index 79943a8b084..774a3fe95f6 100644 --- a/homeassistant/components/sensor/alpha_vantage.py +++ b/homeassistant/components/sensor/alpha_vantage.py @@ -23,7 +23,8 @@ ATTR_CLOSE = 'close' ATTR_HIGH = 'high' ATTR_LOW = 'low' -CONF_ATTRIBUTION = "Stock market information provided by Alpha Vantage" +ATTRIBUTION = "Stock market information provided by Alpha Vantage" + CONF_FOREIGN_EXCHANGE = 'foreign_exchange' CONF_FROM = 'from' CONF_SYMBOL = 'symbol' @@ -143,7 +144,7 @@ class AlphaVantageSensor(Entity): """Return the state attributes.""" if self.values is not None: return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_CLOSE: self.values['4. close'], ATTR_HIGH: self.values['2. high'], ATTR_LOW: self.values['3. low'], @@ -203,7 +204,7 @@ class AlphaVantageForeignExchange(Entity): """Return the state attributes.""" if self.values is not None: return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, CONF_FROM: self._from_currency, CONF_TO: self._to_currency, } diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index 34855d19104..e654f29f42a 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -19,7 +19,7 @@ REQUIREMENTS = ['blockchain==1.4.4'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by blockchain.info" +ATTRIBUTION = "Data provided by blockchain.info" DEFAULT_CURRENCY = 'USD' @@ -112,7 +112,7 @@ class BitcoinSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } def update(self): diff --git a/homeassistant/components/sensor/blockchain.py b/homeassistant/components/sensor/blockchain.py index e51db7edcad..241c98d2328 100644 --- a/homeassistant/components/sensor/blockchain.py +++ b/homeassistant/components/sensor/blockchain.py @@ -18,8 +18,9 @@ REQUIREMENTS = ['python-blockchain-api==0.0.2'] _LOGGER = logging.getLogger(__name__) +ATTRIBUTION = "Data provided by blockchain.info" + CONF_ADDRESSES = 'addresses' -CONF_ATTRIBUTION = "Data provided by blockchain.info" DEFAULT_NAME = 'Bitcoin Balance' @@ -82,7 +83,7 @@ class BlockchainSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } def update(self): diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index df8b5391359..62a3706034a 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -34,7 +34,8 @@ ATTR_STATION_ID = 'station_id' ATTR_STATION_NAME = 'station_name' ATTR_ZONE_ID = 'zone_id' -CONF_ATTRIBUTION = "Data provided by the Australian Bureau of Meteorology" +ATTRIBUTION = "Data provided by the Australian Bureau of Meteorology" + CONF_STATION = 'station' CONF_ZONE_ID = 'zone_id' CONF_WMO_ID = 'wmo_id' @@ -158,7 +159,7 @@ class BOMCurrentSensor(Entity): def device_state_attributes(self): """Return the state attributes of the device.""" attr = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_LAST_UPDATE: self.bom_data.last_updated, ATTR_SENSOR_ID: self._condition, ATTR_STATION_ID: self.bom_data.latest_data['wmo'], diff --git a/homeassistant/components/sensor/coinbase.py b/homeassistant/components/sensor/coinbase.py index d25b7b786f8..54af94944d6 100644 --- a/homeassistant/components/sensor/coinbase.py +++ b/homeassistant/components/sensor/coinbase.py @@ -16,9 +16,10 @@ CURRENCY_ICONS = { 'LTC': 'mdi:litecoin', 'USD': 'mdi:currency-usd' } + DEFAULT_COIN_ICON = 'mdi:coin' -CONF_ATTRIBUTION = "Data provided by coinbase.com" +ATTRIBUTION = "Data provided by coinbase.com" DATA_COINBASE = 'coinbase_cache' DEPENDENCIES = ['coinbase'] @@ -77,7 +78,7 @@ class AccountSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_NATIVE_BALANCE: "{} {}".format( self._native_balance, self._native_currency), } @@ -127,7 +128,7 @@ class ExchangeRateSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION + ATTR_ATTRIBUTION: ATTRIBUTION } def update(self): diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index 18d3f0a3d00..9143405a553 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -32,7 +32,8 @@ ATTR_RANK = 'rank' ATTR_SYMBOL = 'symbol' ATTR_TOTAL_SUPPLY = 'total_supply' -CONF_ATTRIBUTION = "Data provided by CoinMarketCap" +ATTRIBUTION = "Data provided by CoinMarketCap" + CONF_CURRENCY_ID = 'currency_id' CONF_DISPLAY_CURRENCY_DECIMALS = 'display_currency_decimals' @@ -115,7 +116,7 @@ class CoinMarketCapSensor(Entity): ATTR_VOLUME_24H: self._ticker.get('quotes').get(self.data.display_currency) .get('volume_24h'), - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_CIRCULATING_SUPPLY: self._ticker.get('circulating_supply'), ATTR_MARKET_CAP: self._ticker.get('quotes').get(self.data.display_currency) diff --git a/homeassistant/components/sensor/comed_hourly_pricing.py b/homeassistant/components/sensor/comed_hourly_pricing.py index 12b8e917f9d..1771fd0f1a3 100644 --- a/homeassistant/components/sensor/comed_hourly_pricing.py +++ b/homeassistant/components/sensor/comed_hourly_pricing.py @@ -25,7 +25,8 @@ _RESOURCE = 'https://hourlypricing.comed.com/api' SCAN_INTERVAL = timedelta(minutes=5) -CONF_ATTRIBUTION = "Data provided by ComEd Hourly Pricing service" +ATTRIBUTION = "Data provided by ComEd Hourly Pricing service" + CONF_CURRENT_HOUR_AVERAGE = 'current_hour_average' CONF_FIVE_MINUTE = 'five_minute' CONF_MONITORED_FEEDS = 'monitored_feeds' @@ -97,8 +98,7 @@ class ComedHourlyPricingSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} - return attrs + return {ATTR_ATTRIBUTION: ATTRIBUTION} async def async_update(self): """Get the ComEd Hourly Pricing data from the web service.""" diff --git a/homeassistant/components/sensor/currencylayer.py b/homeassistant/components/sensor/currencylayer.py index 67c9c7bbf19..bbc63633c51 100644 --- a/homeassistant/components/sensor/currencylayer.py +++ b/homeassistant/components/sensor/currencylayer.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _RESOURCE = 'http://apilayer.net/api/live' -CONF_ATTRIBUTION = "Data provided by currencylayer.com" +ATTRIBUTION = "Data provided by currencylayer.com" DEFAULT_BASE = 'USD' DEFAULT_NAME = 'CurrencyLayer Sensor' @@ -91,7 +91,7 @@ class CurrencylayerSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } def update(self): diff --git a/homeassistant/components/sensor/discogs.py b/homeassistant/components/sensor/discogs.py index 70d7155fec7..c62cd3e759c 100644 --- a/homeassistant/components/sensor/discogs.py +++ b/homeassistant/components/sensor/discogs.py @@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_IDENTITY = 'identity' -CONF_ATTRIBUTION = "Data provided by Discogs" +ATTRIBUTION = "Data provided by Discogs" DEFAULT_NAME = 'Discogs' @@ -86,7 +86,7 @@ class DiscogsSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_IDENTITY: self._identity.name, } diff --git a/homeassistant/components/sensor/dublin_bus_transport.py b/homeassistant/components/sensor/dublin_bus_transport.py index 02527f1e333..7a70d7af3a7 100644 --- a/homeassistant/components/sensor/dublin_bus_transport.py +++ b/homeassistant/components/sensor/dublin_bus_transport.py @@ -28,7 +28,8 @@ ATTR_DUE_IN = 'Due in' ATTR_DUE_AT = 'Due at' ATTR_NEXT_UP = 'Later Bus' -CONF_ATTRIBUTION = "Data provided by data.dublinked.ie" +ATTRIBUTION = "Data provided by data.dublinked.ie" + CONF_STOP_ID = 'stopid' CONF_ROUTE = 'route' @@ -101,7 +102,7 @@ class DublinPublicTransportSensor(Entity): ATTR_DUE_AT: self._times[0][ATTR_DUE_AT], ATTR_STOP_ID: self._stop, ATTR_ROUTE: self._times[0][ATTR_ROUTE], - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_NEXT_UP: next_up } diff --git a/homeassistant/components/sensor/entur_public_transport.py b/homeassistant/components/sensor/entur_public_transport.py index 64884764523..330f5f8cc56 100644 --- a/homeassistant/components/sensor/entur_public_transport.py +++ b/homeassistant/components/sensor/entur_public_transport.py @@ -26,7 +26,8 @@ ATTR_NEXT_UP_IN = 'next_due_in' API_CLIENT_NAME = 'homeassistant-homeassistant' -CONF_ATTRIBUTION = "Data provided by entur.org under NLOD." +ATTRIBUTION = "Data provided by entur.org under NLOD" + CONF_STOP_IDS = 'stop_ids' CONF_EXPAND_PLATFORMS = 'expand_platforms' CONF_WHITELIST_LINES = 'line_whitelist' @@ -140,7 +141,7 @@ class EnturPublicTransportSensor(Entity): self._state = None self._icon = ICONS[DEFAULT_ICON_KEY] self._attributes = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_STOP_ID: self._stop, } diff --git a/homeassistant/components/sensor/etherscan.py b/homeassistant/components/sensor/etherscan.py index 24cf046cca0..3b76d888e26 100644 --- a/homeassistant/components/sensor/etherscan.py +++ b/homeassistant/components/sensor/etherscan.py @@ -15,10 +15,11 @@ from homeassistant.helpers.entity import Entity REQUIREMENTS = ['python-etherscan-api==0.0.3'] +ATTRIBUTION = "Data provided by etherscan.io" + CONF_ADDRESS = 'address' CONF_TOKEN = 'token' CONF_TOKEN_ADDRESS = 'token_address' -CONF_ATTRIBUTION = "Data provided by etherscan.io" SCAN_INTERVAL = timedelta(minutes=5) @@ -77,9 +78,7 @@ class EtherscanSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - } + return {ATTR_ATTRIBUTION: ATTRIBUTION} def update(self): """Get the latest state of the sensor.""" diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index f5b44d577a7..d5d9150e4e8 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -35,7 +35,7 @@ ATTR_LAST_SAVED_AT = 'last_saved_at' CONF_MONITORED_RESOURCES = 'monitored_resources' CONF_CLOCK_FORMAT = 'clock_format' -CONF_ATTRIBUTION = 'Data provided by Fitbit.com' +ATTRIBUTION = 'Data provided by Fitbit.com' DEPENDENCIES = ['http'] @@ -423,8 +423,8 @@ class FitbitSensor(Entity): """Icon to use in the frontend, if any.""" if self.resource_type == 'devices/battery' and self.extra: battery_level = BATTERY_LEVELS[self.extra.get('battery')] - return icon_for_battery_level(battery_level=battery_level, - charging=None) + return icon_for_battery_level( + battery_level=battery_level, charging=None) return 'mdi:{}'.format(FITBIT_RESOURCES_LIST[self.resource_type][2]) @property @@ -432,7 +432,7 @@ class FitbitSensor(Entity): """Return the state attributes.""" attrs = {} - attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION if self.extra: attrs['model'] = self.extra.get('deviceVersion') diff --git a/homeassistant/components/sensor/fixer.py b/homeassistant/components/sensor/fixer.py index 1bdd9e71272..c46fa751319 100644 --- a/homeassistant/components/sensor/fixer.py +++ b/homeassistant/components/sensor/fixer.py @@ -20,8 +20,8 @@ _LOGGER = logging.getLogger(__name__) ATTR_EXCHANGE_RATE = 'Exchange rate' ATTR_TARGET = 'Target currency' +ATTRIBUTION = "Data provided by the European Central Bank (ECB)" -CONF_ATTRIBUTION = "Data provided by the European Central Bank (ECB)" CONF_TARGET = 'target' DEFAULT_BASE = 'USD' @@ -86,7 +86,7 @@ class ExchangeRateSensor(Entity): """Return the state attributes.""" if self.data.rate is not None: return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_EXCHANGE_RATE: self.data.rate['rates'][self._target], ATTR_TARGET: self._target, } diff --git a/homeassistant/components/sensor/gitlab_ci.py b/homeassistant/components/sensor/gitlab_ci.py index 1e55a7d6997..7f3b444bb75 100644 --- a/homeassistant/components/sensor/gitlab_ci.py +++ b/homeassistant/components/sensor/gitlab_ci.py @@ -28,8 +28,8 @@ ATTR_BUILD_FINISHED = 'build_finished' ATTR_BUILD_ID = 'build id' ATTR_BUILD_STARTED = 'build_started' ATTR_BUILD_STATUS = 'build_status' +ATTRIBUTION = "Information provided by https://gitlab.com/" -CONF_ATTRIBUTION = "Information provided by https://gitlab.com/" CONF_GITLAB_ID = 'gitlab_id' DEFAULT_NAME = 'GitLab CI Status' @@ -101,7 +101,7 @@ class GitLabSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_BUILD_STATUS: self._state, ATTR_BUILD_STARTED: self._started_at, ATTR_BUILD_FINISHED: self._finished_at, diff --git a/homeassistant/components/sensor/haveibeenpwned.py b/homeassistant/components/sensor/haveibeenpwned.py index 4d651ea81c7..a4ae2349e24 100644 --- a/homeassistant/components/sensor/haveibeenpwned.py +++ b/homeassistant/components/sensor/haveibeenpwned.py @@ -12,7 +12,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_EMAIL +from homeassistant.const import CONF_EMAIL, ATTR_ATTRIBUTION import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_point_in_time @@ -21,6 +21,8 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) +ATTRIBUTION = "Data provided by Have I Been Pwned (HIBP)" + DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S" HA_USER_AGENT = "Home Assistant HaveIBeenPwned Sensor Component" @@ -75,7 +77,7 @@ class HaveIBeenPwnedSensor(Entity): @property def device_state_attributes(self): """Return the attributes of the sensor.""" - val = {} + val = {ATTR_ATTRIBUTION: ATTRIBUTION} if self._email not in self._data.data: return val diff --git a/homeassistant/components/sensor/iperf3.py b/homeassistant/components/sensor/iperf3.py index 0eb6dfaa00c..32127c79a91 100644 --- a/homeassistant/components/sensor/iperf3.py +++ b/homeassistant/components/sensor/iperf3.py @@ -25,7 +25,8 @@ ATTR_REMOTE_HOST = 'Remote Server' ATTR_REMOTE_PORT = 'Remote Port' ATTR_VERSION = 'Version' -CONF_ATTRIBUTION = 'Data retrieved using Iperf3' +ATTRIBUTION = 'Data retrieved using Iperf3' + CONF_DURATION = 'duration' CONF_PARALLEL = 'parallel' @@ -106,11 +107,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class Iperf3Sensor(Entity): """A Iperf3 sensor implementation.""" - def __init__(self, server, port, duration, streams, - protocol, sensor_type): + def __init__(self, server, port, duration, streams, protocol, sensor_type): """Initialize the sensor.""" self._attrs = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_PROTOCOL: protocol, } self._name = \ @@ -144,7 +144,7 @@ class Iperf3Sensor(Entity): def device_state_attributes(self): """Return the state attributes.""" if self.result is not None: - self._attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + self._attrs[ATTR_ATTRIBUTION] = ATTRIBUTION self._attrs[ATTR_REMOTE_HOST] = self.result.remote_host self._attrs[ATTR_REMOTE_PORT] = self.result.remote_port self._attrs[ATTR_VERSION] = self.result.version diff --git a/homeassistant/components/sensor/irish_rail_transport.py b/homeassistant/components/sensor/irish_rail_transport.py index 10f4004ae74..e17ecfde59d 100644 --- a/homeassistant/components/sensor/irish_rail_transport.py +++ b/homeassistant/components/sensor/irish_rail_transport.py @@ -1,9 +1,4 @@ -""" -Support for Irish Rail RTPI information. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.irish_rail_transport/ -""" +"""Support for Irish Rail RTPI information.""" import logging from datetime import timedelta @@ -11,7 +6,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION from homeassistant.helpers.entity import Entity REQUIREMENTS = ['pyirishrail==0.0.2'] @@ -28,6 +23,7 @@ ATTR_DUE_AT = "Due at" ATTR_EXPECT_AT = "Expected at" ATTR_NEXT_UP = "Later Train" ATTR_TRAIN_TYPE = "Train type" +ATTRIBUTION = "Data provided by Irish Rail" CONF_STATION = 'station' CONF_DESTINATION = 'destination' @@ -100,6 +96,7 @@ class IrishRailTransportSensor(Entity): next_up += self._times[1][ATTR_DUE_IN] return { + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_STATION: self._station, ATTR_ORIGIN: self._times[0][ATTR_ORIGIN], ATTR_DESTINATION: self._times[0][ATTR_DESTINATION], @@ -109,7 +106,7 @@ class IrishRailTransportSensor(Entity): ATTR_DIRECTION: self._times[0][ATTR_DIRECTION], ATTR_STOPS_AT: self._times[0][ATTR_STOPS_AT], ATTR_NEXT_UP: next_up, - ATTR_TRAIN_TYPE: self._times[0][ATTR_TRAIN_TYPE] + ATTR_TRAIN_TYPE: self._times[0][ATTR_TRAIN_TYPE], } @property @@ -146,22 +143,23 @@ class IrishRailTransportData: def update(self): """Get the latest data from irishrail.""" - trains = self._ir_api.get_station_by_name(self.station, - direction=self.direction, - destination=self.destination, - stops_at=self.stops_at) + trains = self._ir_api.get_station_by_name( + self.station, direction=self.direction, + destination=self.destination, stops_at=self.stops_at) stops_at = self.stops_at if self.stops_at else '' self.info = [] for train in trains: - train_data = {ATTR_STATION: self.station, - ATTR_ORIGIN: train.get('origin'), - ATTR_DESTINATION: train.get('destination'), - ATTR_DUE_IN: train.get('due_in_mins'), - ATTR_DUE_AT: train.get('scheduled_arrival_time'), - ATTR_EXPECT_AT: train.get('expected_departure_time'), - ATTR_DIRECTION: train.get('direction'), - ATTR_STOPS_AT: stops_at, - ATTR_TRAIN_TYPE: train.get('type')} + train_data = { + ATTR_STATION: self.station, + ATTR_ORIGIN: train.get('origin'), + ATTR_DESTINATION: train.get('destination'), + ATTR_DUE_IN: train.get('due_in_mins'), + ATTR_DUE_AT: train.get('scheduled_arrival_time'), + ATTR_EXPECT_AT: train.get('expected_departure_time'), + ATTR_DIRECTION: train.get('direction'), + ATTR_STOPS_AT: stops_at, + ATTR_TRAIN_TYPE: train.get('type'), + } self.info.append(train_data) if not self.info: @@ -180,4 +178,5 @@ class IrishRailTransportData: ATTR_EXPECT_AT: 'n/a', ATTR_DIRECTION: direction, ATTR_STOPS_AT: stops_at, - ATTR_TRAIN_TYPE: ''}] + ATTR_TRAIN_TYPE: '', + }] diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index fa69a916495..bb5a09771c2 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -1,16 +1,11 @@ -""" -Sensor for Last.fm account status. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.lastfm/ -""" +"""Sensor for Last.fm account status.""" import logging import re import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, ATTR_ATTRIBUTION import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -21,6 +16,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_LAST_PLAYED = 'last_played' ATTR_PLAY_COUNT = 'play_count' ATTR_TOP_PLAYED = 'top_played' +ATTRIBUTION = "Data provided by Last.fm" CONF_USERS = 'users' @@ -105,6 +101,7 @@ class LastfmSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" return { + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_LAST_PLAYED: self._lastplayed, ATTR_PLAY_COUNT: self._playcount, ATTR_TOP_PLAYED: self._topplayed, diff --git a/homeassistant/components/sensor/london_underground.py b/homeassistant/components/sensor/london_underground.py index d44806cf481..1c93d6a1bcb 100644 --- a/homeassistant/components/sensor/london_underground.py +++ b/homeassistant/components/sensor/london_underground.py @@ -18,8 +18,11 @@ REQUIREMENTS = ['london-tube-status==0.2'] _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by TfL Open Data" + CONF_LINE = 'line' + SCAN_INTERVAL = timedelta(seconds=30) + TUBE_LINES = [ 'Bakerloo', 'Central', @@ -34,7 +37,8 @@ TUBE_LINES = [ 'Piccadilly', 'TfL Rail', 'Victoria', - 'Waterloo & City'] + 'Waterloo & City', +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_LINE): diff --git a/homeassistant/components/sensor/metoffice.py b/homeassistant/components/sensor/metoffice.py index 8cebecb7124..3d9c9485da3 100644 --- a/homeassistant/components/sensor/metoffice.py +++ b/homeassistant/components/sensor/metoffice.py @@ -26,7 +26,7 @@ ATTR_SENSOR_ID = 'sensor_id' ATTR_SITE_ID = 'site_id' ATTR_SITE_NAME = 'site_name' -CONF_ATTRIBUTION = "Data provided by the Met Office" +ATTRIBUTION = "Data provided by the Met Office" CONDITION_CLASSES = { 'cloudy': ['7', '8'], @@ -162,7 +162,7 @@ class MetOfficeCurrentSensor(Entity): def device_state_attributes(self): """Return the state attributes of the device.""" attr = {} - attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attr[ATTR_ATTRIBUTION] = ATTRIBUTION attr[ATTR_LAST_UPDATE] = self.data.data.date attr[ATTR_SENSOR_ID] = self._condition attr[ATTR_SITE_ID] = self.site.id diff --git a/homeassistant/components/sensor/nederlandse_spoorwegen.py b/homeassistant/components/sensor/nederlandse_spoorwegen.py index 81cfada25f6..5d9376ad9eb 100644 --- a/homeassistant/components/sensor/nederlandse_spoorwegen.py +++ b/homeassistant/components/sensor/nederlandse_spoorwegen.py @@ -21,7 +21,8 @@ REQUIREMENTS = ['nsapi==2.7.4'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by NS" +ATTRIBUTION = "Data provided by NS" + CONF_ROUTES = 'routes' CONF_FROM = 'from' CONF_TO = 'to' @@ -155,7 +156,7 @@ class NSDepartureSensor(Entity): 'transfers': self._trips[0].nr_transfers, 'route': route, 'remarks': [r.message for r in self._trips[0].trip_remarks], - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } @Throttle(MIN_TIME_BETWEEN_UPDATES) diff --git a/homeassistant/components/sensor/nsw_fuel_station.py b/homeassistant/components/sensor/nsw_fuel_station.py index 60ad83a82f2..f0da619a6e7 100644 --- a/homeassistant/components/sensor/nsw_fuel_station.py +++ b/homeassistant/components/sensor/nsw_fuel_station.py @@ -28,7 +28,8 @@ CONF_FUEL_TYPES = 'fuel_types' CONF_ALLOWED_FUEL_TYPES = ["E10", "U91", "E85", "P95", "P98", "DL", "PDL", "B20", "LPG", "CNG", "EV"] CONF_DEFAULT_FUEL_TYPES = ["E10", "U91"] -CONF_ATTRIBUTION = "Data provided by NSW Government FuelCheck" + +ATTRIBUTION = "Data provided by NSW Government FuelCheck" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_STATION_ID): cv.positive_int, @@ -161,7 +162,7 @@ class StationPriceSensor(Entity): return { ATTR_STATION_ID: self._station_data.station_id, ATTR_STATION_NAME: self._station_data.get_station_name(), - ATTR_ATTRIBUTION: CONF_ATTRIBUTION + ATTR_ATTRIBUTION: ATTRIBUTION } @property diff --git a/homeassistant/components/sensor/openexchangerates.py b/homeassistant/components/sensor/openexchangerates.py index 01c84c63034..6361b823dea 100644 --- a/homeassistant/components/sensor/openexchangerates.py +++ b/homeassistant/components/sensor/openexchangerates.py @@ -20,7 +20,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) _RESOURCE = 'https://openexchangerates.org/api/latest.json' -CONF_ATTRIBUTION = "Data provided by openexchangerates.org" +ATTRIBUTION = "Data provided by openexchangerates.org" DEFAULT_BASE = 'USD' DEFAULT_NAME = 'Exchange Rate Sensor' @@ -82,7 +82,7 @@ class OpenexchangeratesSensor(Entity): def device_state_attributes(self): """Return other attributes of the sensor.""" attr = self.rest.data - attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attr[ATTR_ATTRIBUTION] = ATTRIBUTION return attr diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index b6a4ff0860e..a137836138b 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -21,7 +21,8 @@ REQUIREMENTS = ['pyowm==2.10.0'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by OpenWeatherMap" +ATTRIBUTION = "Data provided by OpenWeatherMap" + CONF_FORECAST = 'forecast' CONF_LANGUAGE = 'language' @@ -121,7 +122,7 @@ class OpenWeatherMapSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } def update(self): diff --git a/homeassistant/components/sensor/rejseplanen.py b/homeassistant/components/sensor/rejseplanen.py index bade1bd6315..7a8cddb6179 100755 --- a/homeassistant/components/sensor/rejseplanen.py +++ b/homeassistant/components/sensor/rejseplanen.py @@ -31,7 +31,8 @@ ATTR_DUE_IN = 'Due in' ATTR_DUE_AT = 'Due at' ATTR_NEXT_UP = 'Later departure' -CONF_ATTRIBUTION = "Data provided by rejseplanen.dk" +ATTRIBUTION = "Data provided by rejseplanen.dk" + CONF_STOP_ID = 'stop_id' CONF_ROUTE = 'route' CONF_DIRECTION = 'direction' @@ -50,8 +51,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DIRECTION, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_DEPARTURE_TYPE, default=[]): - vol.All(cv.ensure_list, [vol.In(list(['BUS', 'EXB', 'M', - 'S', 'REG']))]) + vol.All(cv.ensure_list, + [vol.In(list(['BUS', 'EXB', 'M', 'S', 'REG']))]) }) @@ -75,12 +76,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): departure_type = config[CONF_DEPARTURE_TYPE] data = PublicTransportData(stop_id, route, direction, departure_type) - add_devices([RejseplanenTransportSensor(data, - stop_id, - route, - direction, - name)], - True) + add_devices([RejseplanenTransportSensor( + data, stop_id, route, direction, name)], True) class RejseplanenTransportSensor(Entity): @@ -111,13 +108,11 @@ class RejseplanenTransportSensor(Entity): if self._times is not None: next_up = None if len(self._times) > 1: - next_up = ('{} towards ' - '{} in ' - '{} from ' - '{}'.format(self._times[1][ATTR_ROUTE], - self._times[1][ATTR_DIRECTION], - str(self._times[1][ATTR_DUE_IN]), - self._times[1][ATTR_STOP_NAME])) + next_up = ('{} towards {} in {} from {}'.format( + self._times[1][ATTR_ROUTE], + self._times[1][ATTR_DIRECTION], + str(self._times[1][ATTR_DUE_IN]), + self._times[1][ATTR_STOP_NAME])) params = { ATTR_DUE_IN: str(self._times[0][ATTR_DUE_IN]), ATTR_DUE_AT: self._times[0][ATTR_DUE_AT], @@ -126,9 +121,9 @@ class RejseplanenTransportSensor(Entity): ATTR_DIRECTION: self._times[0][ATTR_DIRECTION], ATTR_STOP_NAME: self._times[0][ATTR_STOP_NAME], ATTR_STOP_ID: self._stop_id, - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_NEXT_UP: next_up - } + } return {k: v for k, v in params.items() if v} @property diff --git a/homeassistant/components/sensor/ring.py b/homeassistant/components/sensor/ring.py index 9478768f889..d58e0cf8b3f 100644 --- a/homeassistant/components/sensor/ring.py +++ b/homeassistant/components/sensor/ring.py @@ -11,7 +11,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.ring import ( - CONF_ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DATA_RING) + ATTRIBUTION, DEFAULT_ENTITY_NAMESPACE, DATA_RING) from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS, @@ -122,7 +122,7 @@ class RingSensor(Entity): """Return the state attributes.""" attrs = {} - attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION attrs['device_id'] = self._data.id attrs['firmware'] = self._data.firmware attrs['kind'] = self._data.kind diff --git a/homeassistant/components/sensor/ripple.py b/homeassistant/components/sensor/ripple.py index beb7bf22269..4abbc4efe06 100644 --- a/homeassistant/components/sensor/ripple.py +++ b/homeassistant/components/sensor/ripple.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity REQUIREMENTS = ['python-ripple-api==0.0.3'] CONF_ADDRESS = 'address' -CONF_ATTRIBUTION = "Data provided by ripple.com" +ATTRIBUTION = "Data provided by ripple.com" DEFAULT_NAME = 'Ripple Balance' @@ -65,7 +65,7 @@ class RippleSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } def update(self): diff --git a/homeassistant/components/sensor/sochain.py b/homeassistant/components/sensor/sochain.py index b582ba04567..d5bd4e5da82 100644 --- a/homeassistant/components/sensor/sochain.py +++ b/homeassistant/components/sensor/sochain.py @@ -19,9 +19,10 @@ REQUIREMENTS = ['python-sochain-api==0.0.2'] _LOGGER = logging.getLogger(__name__) +ATTRIBUTION = "Data provided by chain.so" + CONF_ADDRESS = 'address' CONF_NETWORK = 'network' -CONF_ATTRIBUTION = "Data provided by chain.so" DEFAULT_NAME = 'Crypto Balance' @@ -34,8 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the sochain sensors.""" from pysochain import ChainSo address = config.get(CONF_ADDRESS) @@ -77,7 +78,7 @@ class SochainSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } async def async_update(self): diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 6b34930075a..d9f2410f8ca 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -30,7 +30,8 @@ ATTR_TARGET = 'destination' ATTR_TRAIN_NUMBER = 'train_number' ATTR_TRANSFERS = 'transfers' -CONF_ATTRIBUTION = "Data provided by transport.opendata.ch" +ATTRIBUTION = "Data provided by transport.opendata.ch" + CONF_DESTINATION = 'to' CONF_START = 'from' @@ -113,7 +114,7 @@ class SwissPublicTransportSensor(Entity): ATTR_START: self._opendata.from_name, ATTR_TARGET: self._opendata.to_name, ATTR_REMAINING_TIME: '{}'.format(self._remaining_time), - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } return attr diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py index 39a9e75c47b..2b443738230 100644 --- a/homeassistant/components/sensor/synologydsm.py +++ b/homeassistant/components/sensor/synologydsm.py @@ -1,9 +1,4 @@ -""" -Support for Synology NAS Sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.synologydsm/ -""" +"""Support for Synology NAS Sensors.""" import logging from datetime import timedelta @@ -22,7 +17,8 @@ REQUIREMENTS = ['python-synology==0.2.0'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = 'Data provided by Synology' +ATTRIBUTION = 'Data provided by Synology' + CONF_VOLUMES = 'volumes' DEFAULT_NAME = 'Synology DSM' DEFAULT_PORT = 5001 @@ -194,7 +190,7 @@ class SynoNasSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } diff --git a/homeassistant/components/sensor/sytadin.py b/homeassistant/components/sensor/sytadin.py index 082342a0393..f8ef18fcffe 100644 --- a/homeassistant/components/sensor/sytadin.py +++ b/homeassistant/components/sensor/sytadin.py @@ -24,8 +24,7 @@ _LOGGER = logging.getLogger(__name__) URL = 'http://www.sytadin.fr/sys/barometres_de_la_circulation.jsp.html' -CONF_ATTRIBUTION = "Data provided by Direction des routes Île-de-France" \ - "(DiRIF)" +ATTRIBUTION = "Data provided by Direction des routes Île-de-France (DiRIF)" DEFAULT_NAME = 'Sytadin' REGEX = r'(\d*\.\d+|\d+)' @@ -95,7 +94,7 @@ class SytadinSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } def update(self): diff --git a/homeassistant/components/sensor/trafikverket_weatherstation.py b/homeassistant/components/sensor/trafikverket_weatherstation.py index 433bb8e9ed1..38cc23dabbe 100644 --- a/homeassistant/components/sensor/trafikverket_weatherstation.py +++ b/homeassistant/components/sensor/trafikverket_weatherstation.py @@ -24,12 +24,13 @@ REQUIREMENTS = ['pytrafikverket==0.1.5.8'] _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=300) -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) +ATTRIBUTION = "Data provided by Trafikverket API" -CONF_ATTRIBUTION = "Data provided by Trafikverket API" CONF_STATION = 'station' +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) + +SCAN_INTERVAL = timedelta(seconds=300) SENSOR_TYPES = { 'air_temp': ['Air temperature', '°C', 'air_temp'], @@ -50,8 +51,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Trafikverket sensor platform.""" from pytrafikverket.trafikverket_weather import TrafikverketWeather @@ -85,7 +86,7 @@ class TrafikverketWeatherStation(Entity): self._station = sensor_station self._weather_api = weather_api self._attributes = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } self._weather = None diff --git a/homeassistant/components/sensor/transport_nsw.py b/homeassistant/components/sensor/transport_nsw.py index 2e28d81a2c3..3c40bf4f709 100644 --- a/homeassistant/components/sensor/transport_nsw.py +++ b/homeassistant/components/sensor/transport_nsw.py @@ -1,9 +1,4 @@ -""" -Transport NSW (AU) sensor to query next leave event for a specified stop. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.transport_nsw/ -""" +"""Support for Transport NSW (AU) to query next leave event.""" from datetime import timedelta import logging @@ -26,7 +21,8 @@ ATTR_REAL_TIME = 'real_time' ATTR_DESTINATION = 'destination' ATTR_MODE = 'mode' -CONF_ATTRIBUTION = "Data provided by Transport NSW" +ATTRIBUTION = "Data provided by Transport NSW" + CONF_STOP_ID = 'stop_id' CONF_ROUTE = 'route' CONF_DESTINATION = 'destination' @@ -40,7 +36,7 @@ ICONS = { 'Ferry': 'mdi:ferry', 'Schoolbus': 'mdi:bus', 'n/a': 'mdi:clock', - None: 'mdi:clock' + None: 'mdi:clock', } SCAN_INTERVAL = timedelta(seconds=60) @@ -99,7 +95,7 @@ class TransportNSWSensor(Entity): ATTR_REAL_TIME: self._times[ATTR_REAL_TIME], ATTR_DESTINATION: self._times[ATTR_DESTINATION], ATTR_MODE: self._times[ATTR_MODE], - ATTR_ATTRIBUTION: CONF_ATTRIBUTION + ATTR_ATTRIBUTION: ATTRIBUTION } @property diff --git a/homeassistant/components/sensor/travisci.py b/homeassistant/components/sensor/travisci.py index e1bd74b993c..c96bb18e958 100644 --- a/homeassistant/components/sensor/travisci.py +++ b/homeassistant/components/sensor/travisci.py @@ -20,7 +20,8 @@ REQUIREMENTS = ['TravisPy==0.3.5'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Information provided by https://travis-ci.org/" +ATTRIBUTION = "Information provided by https://travis-ci.org/" + CONF_BRANCH = 'branch' CONF_REPOSITORY = 'repository' @@ -130,7 +131,7 @@ class TravisCISensor(Entity): def device_state_attributes(self): """Return the state attributes.""" attrs = {} - attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION if self._build and self._state is not None: if self._user and self._sensor_type == 'state': diff --git a/homeassistant/components/sensor/vasttrafik.py b/homeassistant/components/sensor/vasttrafik.py index 124b0ff44ea..8148a5c2fc7 100644 --- a/homeassistant/components/sensor/vasttrafik.py +++ b/homeassistant/components/sensor/vasttrafik.py @@ -24,8 +24,8 @@ ATTR_ACCESSIBILITY = 'accessibility' ATTR_DIRECTION = 'direction' ATTR_LINE = 'line' ATTR_TRACK = 'track' +ATTRIBUTION = "Data provided by Västtrafik" -CONF_ATTRIBUTION = "Data provided by Västtrafik" CONF_DELAY = 'delay' CONF_DEPARTURES = 'departures' CONF_FROM = 'from' @@ -137,7 +137,7 @@ class VasttrafikDepartureSensor(Entity): params = { ATTR_ACCESSIBILITY: departure.get('accessibility'), - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_DIRECTION: departure.get('direction'), ATTR_LINE: departure.get('sname'), ATTR_TRACK: departure.get('track'), diff --git a/homeassistant/components/sensor/viaggiatreno.py b/homeassistant/components/sensor/viaggiatreno.py index 82068c456b6..2b8de2042fa 100644 --- a/homeassistant/components/sensor/viaggiatreno.py +++ b/homeassistant/components/sensor/viaggiatreno.py @@ -18,7 +18,8 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Powered by ViaggiaTreno Data" +ATTRIBUTION = "Powered by ViaggiaTreno Data" + VIAGGIATRENO_ENDPOINT = ("http://www.viaggiatreno.it/viaggiatrenonew/" "resteasy/viaggiatreno/andamentoTreno/" "{station_id}/{train_id}") @@ -35,7 +36,7 @@ MONITORED_INFO = [ 'orarioPartenza', 'origine', 'subTitle', - ] +] DEFAULT_NAME = "Train {}" @@ -121,7 +122,7 @@ class ViaggiaTrenoSensor(Entity): @property def device_state_attributes(self): """Return extra attributes.""" - self._attributes[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + self._attributes[ATTR_ATTRIBUTION] = ATTRIBUTION return self._attributes @staticmethod diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index ae38c529fe2..6634339be4b 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -26,7 +26,8 @@ ATTR_DURATION = 'duration' ATTR_DISTANCE = 'distance' ATTR_ROUTE = 'route' -CONF_ATTRIBUTION = "Powered by Waze" +ATTRIBUTION = "Powered by Waze" + CONF_DESTINATION = 'destination' CONF_ORIGIN = 'origin' CONF_INCL_FILTER = 'incl_filter' @@ -138,7 +139,7 @@ class WazeTravelTime(Entity): if self._state is None: return None - res = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + res = {ATTR_ATTRIBUTION: ATTRIBUTION} if 'duration' in self._state: res[ATTR_DURATION] = self._state['duration'] if 'distance' in self._state: diff --git a/homeassistant/components/sensor/worldtidesinfo.py b/homeassistant/components/sensor/worldtidesinfo.py index fea3e92a140..0f7bfeaa900 100644 --- a/homeassistant/components/sensor/worldtidesinfo.py +++ b/homeassistant/components/sensor/worldtidesinfo.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by WorldTides" +ATTRIBUTION = "Data provided by WorldTides" DEFAULT_NAME = 'WorldTidesInfo' @@ -72,7 +72,7 @@ class WorldTidesInfoSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of this device.""" - attr = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + attr = {ATTR_ATTRIBUTION: ATTRIBUTION} if 'High' in str(self.data['extremes'][0]['type']): attr['high_tide_time_utc'] = self.data['extremes'][0]['date'] diff --git a/homeassistant/components/sensor/wsdot.py b/homeassistant/components/sensor/wsdot.py index 84f2e8622c6..4e53a2c17c4 100644 --- a/homeassistant/components/sensor/wsdot.py +++ b/homeassistant/components/sensor/wsdot.py @@ -26,7 +26,7 @@ ATTR_DESCRIPTION = 'Description' ATTR_TIME_UPDATED = 'TimeUpdated' ATTR_TRAVEL_TIME_ID = 'TravelTimeID' -CONF_ATTRIBUTION = "Data provided by WSDOT" +ATTRIBUTION = "Data provided by WSDOT" CONF_TRAVEL_TIMES = 'travel_time' @@ -115,7 +115,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): def device_state_attributes(self): """Return other details about the sensor state.""" if self._data is not None: - attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} for key in [ATTR_AVG_TIME, ATTR_NAME, ATTR_DESCRIPTION, ATTR_TRAVEL_TIME_ID]: attrs[key] = self._data.get(key) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index be42e10e894..74a4c2089b2 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -29,7 +29,8 @@ import homeassistant.helpers.config_validation as cv _RESOURCE = 'http://api.wunderground.com/api/{}/{}/{}/q/' _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by the WUnderground weather service" +ATTRIBUTION = "Data provided by the WUnderground weather service" + CONF_PWS_ID = 'pws_id' CONF_LANG = 'lang' @@ -679,9 +680,7 @@ class WUndergroundSensor(Entity): self.rest = rest self._condition = condition self._state = None - self._attributes = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - } + self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} self._icon = None self._entity_picture = None self._unit_of_measurement = self._cfg_expand("unit_of_measurement") diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 0cb9c3765ec..665c482f050 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -29,8 +29,8 @@ REQUIREMENTS = ['xmltodict==0.11.0'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Weather forecast from met.no, delivered " \ - "by the Norwegian Meteorological Institute." +ATTRIBUTION = "Weather forecast from met.no, delivered by the Norwegian " \ + "Meteorological Institute." # https://api.met.no/license_data.html SENSOR_TYPES = { @@ -134,7 +134,7 @@ class YrSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, } @property diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py index 243329680e1..349ee2c7aae 100644 --- a/homeassistant/components/sensor/yweather.py +++ b/homeassistant/components/sensor/yweather.py @@ -21,7 +21,8 @@ REQUIREMENTS = ['yahooweather==0.10'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Weather details provided by Yahoo! Inc." +ATTRIBUTION = "Weather details provided by Yahoo! Inc." + CONF_FORECAST = 'forecast' CONF_WOEID = 'woeid' @@ -131,7 +132,7 @@ class YahooWeatherSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} if self._code is not None and "weather" in self._type: attrs['condition_code'] = self._code diff --git a/homeassistant/components/sensor/zestimate.py b/homeassistant/components/sensor/zestimate.py index a04df22cf07..ed3af84d396 100644 --- a/homeassistant/components/sensor/zestimate.py +++ b/homeassistant/components/sensor/zestimate.py @@ -22,8 +22,9 @@ REQUIREMENTS = ['xmltodict==0.11.0'] _LOGGER = logging.getLogger(__name__) _RESOURCE = 'http://www.zillow.com/webservice/GetZestimate.htm' +ATTRIBUTION = "Data provided by Zillow.com" + CONF_ZPID = 'zpid' -CONF_ATTRIBUTION = "Data provided by Zillow.com" DEFAULT_NAME = 'Zestimate' NAME = 'zestimate' @@ -93,7 +94,7 @@ class ZestimateDataSensor(Entity): if self.data is not None: attributes = self.data attributes['address'] = self.address - attributes[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attributes[ATTR_ATTRIBUTION] = ATTRIBUTION return attributes @property diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index 8724f7d3d66..31d1339fbcf 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -13,7 +13,7 @@ REQUIREMENTS = ['skybellpy==0.3.0'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by Skybell.com" +ATTRIBUTION = "Data provided by Skybell.com" NOTIFICATION_ID = 'skybell_notification' NOTIFICATION_TITLE = 'Skybell Sensor Setup' @@ -76,7 +76,7 @@ class SkybellDevice(Entity): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, 'device_id': self._device.device_id, 'status': self._device.status, 'location': self._device.location, diff --git a/homeassistant/components/weather/met.py b/homeassistant/components/weather/met.py index 90978314a20..3ddbe5aed75 100644 --- a/homeassistant/components/weather/met.py +++ b/homeassistant/components/weather/met.py @@ -17,8 +17,8 @@ REQUIREMENTS = ['pyMetno==0.4.5'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Weather forecast from met.no, delivered " \ - "by the Norwegian Meteorological Institute." +ATTRIBUTION = "Weather forecast from met.no, delivered by the Norwegian " \ + "Meteorological Institute." DEFAULT_NAME = "Met.no" URL = 'https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/' @@ -50,8 +50,8 @@ async def async_setup_platform(hass, config, async_add_entities, 'msl': str(elevation), } - async_add_entities([MetWeather(name, coordinates, - async_get_clientsession(hass))]) + async_add_entities([MetWeather( + name, coordinates, async_get_clientsession(hass))]) class MetWeather(WeatherEntity): @@ -61,18 +61,16 @@ class MetWeather(WeatherEntity): """Initialise the platform with a data instance and site.""" import metno self._name = name - self._weather_data = metno.MetWeatherData(coordinates, - clientsession, - URL - ) + self._weather_data = metno.MetWeatherData( + coordinates, clientsession, URL) self._current_weather_data = {} self._forecast_data = None async def async_added_to_hass(self): """Start fetching data.""" await self._fetch_data() - async_track_utc_time_change(self.hass, self._update, - minute=31, second=0) + async_track_utc_time_change( + self.hass, self._update, minute=31, second=0) async def _fetch_data(self, *_): """Get the latest data from met.no.""" @@ -141,7 +139,7 @@ class MetWeather(WeatherEntity): @property def attribution(self): """Return the attribution.""" - return CONF_ATTRIBUTION + return ATTRIBUTION @property def forecast(self): diff --git a/homeassistant/components/weather/metoffice.py b/homeassistant/components/weather/metoffice.py index 14738980482..3b52eebcff6 100644 --- a/homeassistant/components/weather/metoffice.py +++ b/homeassistant/components/weather/metoffice.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components.sensor.metoffice import ( - CONDITION_CLASSES, CONF_ATTRIBUTION, MetOfficeCurrentData) + CONDITION_CLASSES, ATTRIBUTION, MetOfficeCurrentData) from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS) @@ -118,4 +118,4 @@ class MetOfficeWeather(WeatherEntity): @property def attribution(self): """Return the attribution.""" - return CONF_ATTRIBUTION + return ATTRIBUTION From ab7fda4286bf4e58987266790b8538cf2ff8af43 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Feb 2019 15:54:38 -0800 Subject: [PATCH 009/253] Check against unlinked user (#21081) --- homeassistant/components/person/__init__.py | 8 ++++-- tests/components/person/test_init.py | 29 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index d11d4208dc8..63e588f911b 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -134,22 +134,26 @@ class PersonManager: entities.append(Person(person_conf, False)) + # To make sure IDs don't overlap between config/storage + seen_persons = set(self.config_data) + for person_conf in storage_data.values(): person_id = person_conf[CONF_ID] user_id = person_conf[CONF_USER_ID] - if user_id in self.config_data: + if person_id in seen_persons: _LOGGER.error( "Skipping adding person from storage with same ID as" " configuration.yaml entry: %s", person_id) continue - if user_id in seen_users: + if user_id is not None and user_id in seen_users: _LOGGER.error( "Duplicate user_id %s detected for person %s", user_id, person_id) continue + # To make sure all users have just 1 person linked. seen_users.add(user_id) entities.append(Person(person_conf, True)) diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 2eacb162f8e..f2d796fb204 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -287,6 +287,35 @@ async def test_load_person_storage(hass, hass_admin_user, storage_setup): assert state.attributes.get(ATTR_USER_ID) == hass_admin_user.id +async def test_load_person_storage_two_nonlinked(hass, hass_storage): + """Test loading two users with both not having a user linked.""" + hass_storage[DOMAIN] = { + 'key': DOMAIN, + 'version': 1, + 'data': { + 'persons': [ + { + 'id': '1234', + 'name': 'tracked person 1', + 'user_id': None, + 'device_trackers': [] + }, + { + 'id': '5678', + 'name': 'tracked person 2', + 'user_id': None, + 'device_trackers': [] + }, + ] + } + } + await async_setup_component(hass, DOMAIN, {}) + + assert len(hass.states.async_entity_ids('person')) == 2 + assert hass.states.get('person.tracked_person_1') is not None + assert hass.states.get('person.tracked_person_2') is not None + + async def test_ws_list(hass, hass_ws_client, storage_setup): """Test listing via WS.""" manager = hass.data[DOMAIN] From c115c89afd78d158a3833bbba7b2faec1a9742d3 Mon Sep 17 00:00:00 2001 From: John Mihalic <2854333+mezz64@users.noreply.github.com> Date: Fri, 15 Feb 2019 01:18:12 -0500 Subject: [PATCH 010/253] Bump pyHik library to 0.2.2, improve connections, add sensors (#21086) --- homeassistant/components/binary_sensor/hikvision.py | 4 +++- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/hikvision.py b/homeassistant/components/binary_sensor/hikvision.py index 57618ca2652..fdefc40d8fd 100644 --- a/homeassistant/components/binary_sensor/hikvision.py +++ b/homeassistant/components/binary_sensor/hikvision.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE) -REQUIREMENTS = ['pyhik==0.1.9'] +REQUIREMENTS = ['pyhik==0.2.2'] _LOGGER = logging.getLogger(__name__) CONF_IGNORED = 'ignored' @@ -51,6 +51,8 @@ DEVICE_CLASS_MAP = { 'Unattended Baggage': 'motion', 'Attended Baggage': 'motion', 'Recording Failure': None, + 'Exiting Region': 'motion', + 'Entering Region': 'motion', } CUSTOMIZE_SCHEMA = vol.Schema({ diff --git a/requirements_all.txt b/requirements_all.txt index 01a2b576e5a..592afdd634f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1053,7 +1053,7 @@ pygtt==1.1.2 pyhaversion==2.0.3 # homeassistant.components.binary_sensor.hikvision -pyhik==0.1.9 +pyhik==0.2.2 # homeassistant.components.hive pyhiveapi==0.2.17 From b7607ff472f5cd70cd9b2e6276ded7f1f5cad975 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 15 Feb 2019 10:51:52 +0100 Subject: [PATCH 011/253] Fix tests --- tests/test_setup.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/test_setup.py b/tests/test_setup.py index 8575b023d37..1a60943a72d 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -90,7 +90,7 @@ class TestSetup: } }) - def test_validate_platform_config(self): + def test_validate_platform_config(self, caplog): """Test validating platform configuration.""" platform_schema = PLATFORM_SCHEMA.extend({ 'hello': str, @@ -109,7 +109,7 @@ class TestSetup: MockPlatform('whatever', platform_schema=platform_schema)) - with assert_setup_component(0): + with assert_setup_component(1): assert setup.setup_component(self.hass, 'platform_conf', { 'platform_conf': { 'platform': 'whatever', @@ -117,11 +117,12 @@ class TestSetup: 'invalid': 'extra', } }) + assert caplog.text.count('Your configuration contains extra keys') == 1 self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') - with assert_setup_component(1): + with assert_setup_component(2): assert setup.setup_component(self.hass, 'platform_conf', { 'platform_conf': { 'platform': 'whatever', @@ -132,6 +133,7 @@ class TestSetup: 'invalid': True } }) + assert caplog.text.count('Your configuration contains extra keys') == 2 self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') @@ -183,7 +185,7 @@ class TestSetup: assert 'platform_conf' in self.hass.config.components assert not config['platform_conf'] # empty - def test_validate_platform_config_2(self): + def test_validate_platform_config_2(self, caplog): """Test component PLATFORM_SCHEMA_BASE prio over PLATFORM_SCHEMA.""" platform_schema = PLATFORM_SCHEMA.extend({ 'hello': str, @@ -204,7 +206,7 @@ class TestSetup: MockPlatform('whatever', platform_schema=platform_schema)) - with assert_setup_component(0): + with assert_setup_component(1): assert setup.setup_component(self.hass, 'platform_conf', { # fail: no extra keys allowed in platform schema 'platform_conf': { @@ -213,6 +215,7 @@ class TestSetup: 'invalid': 'extra', } }) + assert caplog.text.count('Your configuration contains extra keys') == 1 self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') @@ -234,7 +237,7 @@ class TestSetup: self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') - def test_validate_platform_config_3(self): + def test_validate_platform_config_3(self, caplog): """Test fallback to component PLATFORM_SCHEMA.""" component_schema = PLATFORM_SCHEMA_BASE.extend({ 'hello': str, @@ -255,15 +258,15 @@ class TestSetup: MockPlatform('whatever', platform_schema=platform_schema)) - with assert_setup_component(0): + with assert_setup_component(1): assert setup.setup_component(self.hass, 'platform_conf', { 'platform_conf': { - # fail: no extra keys allowed 'platform': 'whatever', 'hello': 'world', 'invalid': 'extra', } }) + assert caplog.text.count('Your configuration contains extra keys') == 1 self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') From f6ae054e9fc884b6a3a6c5dab1e9764a904cbc19 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 15 Feb 2019 10:57:02 +0100 Subject: [PATCH 012/253] Lint --- homeassistant/helpers/config_validation.py | 5 ++++- tests/test_setup.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 473fcc88717..f10cf9e057e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -636,7 +636,9 @@ def key_dependency(key, dependency): # Schemas class HASchema(vol.Schema): """Schema class that allows us to mark PREVENT_EXTRA errors as warnings.""" + def __call__(self, data): + """Override __call__ to mark PREVENT_EXTRA as warning.""" try: return super().__call__(data) except vol.Invalid as orig_err: @@ -645,6 +647,7 @@ class HASchema(vol.Schema): # orig_error is of type vol.MultipleInvalid (see super __call__) assert isinstance(orig_err, vol.MultipleInvalid) + # pylint: disable=no-member # If it fails with PREVENT_EXTRA, try with ALLOW_EXTRA self.extra = vol.ALLOW_EXTRA # In case it still fails the following will raise @@ -675,7 +678,7 @@ class HASchema(vol.Schema): return validated def extend(self, schema, required=None, extra=None): - """Extend this schema and convert it to HASchema if necessary""" + """Extend this schema and convert it to HASchema if necessary.""" ret = super().extend(schema, required=required, extra=extra) if extra is not None: return ret diff --git a/tests/test_setup.py b/tests/test_setup.py index 1a60943a72d..c6126bc4a3b 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -117,7 +117,8 @@ class TestSetup: 'invalid': 'extra', } }) - assert caplog.text.count('Your configuration contains extra keys') == 1 + assert caplog.text.count('Your configuration contains ' + 'extra keys') == 1 self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') @@ -133,7 +134,8 @@ class TestSetup: 'invalid': True } }) - assert caplog.text.count('Your configuration contains extra keys') == 2 + assert caplog.text.count('Your configuration contains ' + 'extra keys') == 2 self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') @@ -215,7 +217,8 @@ class TestSetup: 'invalid': 'extra', } }) - assert caplog.text.count('Your configuration contains extra keys') == 1 + assert caplog.text.count('Your configuration contains ' + 'extra keys') == 1 self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') @@ -266,7 +269,8 @@ class TestSetup: 'invalid': 'extra', } }) - assert caplog.text.count('Your configuration contains extra keys') == 1 + assert caplog.text.count('Your configuration contains ' + 'extra keys') == 1 self.hass.data.pop(setup.DATA_SETUP) self.hass.config.components.remove('platform_conf') From eb573c270191eb65c98527b992c25c070b9e8557 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 15 Feb 2019 10:57:47 +0100 Subject: [PATCH 013/253] Meteo france (#21065) * Move files * Move file * Update .coveragerc * Sort import and update file header * Minor changes --- .coveragerc | 4 +- .../__init__.py} | 48 ++++++++----------- .../sensor.py} | 45 ++++++----------- .../weather.py} | 23 +++++---- 4 files changed, 46 insertions(+), 74 deletions(-) rename homeassistant/components/{meteo_france.py => meteo_france/__init__.py} (81%) rename homeassistant/components/{sensor/meteo_france.py => meteo_france/sensor.py} (55%) rename homeassistant/components/{weather/meteo_france.py => meteo_france/weather.py} (83%) diff --git a/.coveragerc b/.coveragerc index 1302ee911a3..4e06eaaef29 100644 --- a/.coveragerc +++ b/.coveragerc @@ -317,6 +317,7 @@ omit = homeassistant/components/media_player/yamaha_musiccast.py homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/ziggo_mediabox_xl.py + homeassistant/components/meteo_france/* homeassistant/components/mochad/* homeassistant/components/modbus/* homeassistant/components/mychevy/* @@ -326,7 +327,6 @@ omit = homeassistant/components/nest/* homeassistant/components/netatmo/* homeassistant/components/netgear_lte/* - homeassistant/components/meteo_france.py homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sqs.py @@ -483,7 +483,6 @@ omit = homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/lyft.py homeassistant/components/sensor/magicseaweed.py - homeassistant/components/sensor/meteo_france.py homeassistant/components/sensor/metoffice.py homeassistant/components/sensor/miflora.py homeassistant/components/sensor/mitemp_bt.py @@ -651,7 +650,6 @@ omit = homeassistant/components/weather/buienradar.py homeassistant/components/weather/darksky.py homeassistant/components/weather/met.py - homeassistant/components/weather/meteo_france.py homeassistant/components/weather/metoffice.py homeassistant/components/weather/openweathermap.py homeassistant/components/weather/zamg.py diff --git a/homeassistant/components/meteo_france.py b/homeassistant/components/meteo_france/__init__.py similarity index 81% rename from homeassistant/components/meteo_france.py rename to homeassistant/components/meteo_france/__init__.py index fa68021d91c..e084cff3c79 100644 --- a/homeassistant/components/meteo_france.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -1,29 +1,27 @@ -""" -Support for Meteo France weather forecast. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/meteo_france/ -""" -import logging +"""Support for Meteo-France weather data.""" import datetime +import logging import voluptuous as vol -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, TEMP_CELSIUS) -from homeassistant.util import Throttle -from homeassistant.helpers.discovery import load_platform +from homeassistant.const import CONF_MONITORED_CONDITIONS, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.util import Throttle REQUIREMENTS = ['meteofrance==0.3.4'] + _LOGGER = logging.getLogger(__name__) -DOMAIN = 'meteo_france' -SCAN_INTERVAL = datetime.timedelta(minutes=5) ATTRIBUTION = "Data provided by Météo-France" + CONF_CITY = 'city' -DEFAULT_WEATHER_CARD = True + DATA_METEO_FRANCE = 'data_meteo_france' +DEFAULT_WEATHER_CARD = True +DOMAIN = 'meteo_france' + +SCAN_INTERVAL = datetime.timedelta(minutes=5) SENSOR_TYPES = { 'rain_chance': ['Rain chance', '%'], @@ -93,9 +91,9 @@ def setup(hass, config): _LOGGER.error(exp) return - client.need_rain_forecast = bool(CONF_MONITORED_CONDITIONS in location - and 'next_rain' in - location[CONF_MONITORED_CONDITIONS]) + client.need_rain_forecast = bool( + CONF_MONITORED_CONDITIONS in location and 'next_rain' in + location[CONF_MONITORED_CONDITIONS]) hass.data[DATA_METEO_FRANCE][city] = MeteoFranceUpdater(client) hass.data[DATA_METEO_FRANCE][city].update() @@ -103,19 +101,11 @@ def setup(hass, config): if CONF_MONITORED_CONDITIONS in location: monitored_conditions = location[CONF_MONITORED_CONDITIONS] load_platform( - hass, - 'sensor', - DOMAIN, - {CONF_CITY: city, - CONF_MONITORED_CONDITIONS: monitored_conditions}, - config) + hass, 'sensor', DOMAIN, { + CONF_CITY: city, + CONF_MONITORED_CONDITIONS: monitored_conditions}, config) - load_platform( - hass, - 'weather', - DOMAIN, - {CONF_CITY: city}, - config) + load_platform(hass, 'weather', DOMAIN, {CONF_CITY: city}, config) return True diff --git a/homeassistant/components/sensor/meteo_france.py b/homeassistant/components/meteo_france/sensor.py similarity index 55% rename from homeassistant/components/sensor/meteo_france.py rename to homeassistant/components/meteo_france/sensor.py index 12933c02e81..f0ef926793e 100644 --- a/homeassistant/components/sensor/meteo_france.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -1,18 +1,9 @@ -""" -Support for Meteo France raining forecast. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.meteo_france/ -""" +"""Support for Meteo-France raining forecast sensor.""" import logging - -from homeassistant.components.meteo_france import (SENSOR_TYPES, - DATA_METEO_FRANCE, - CONF_CITY, - ATTRIBUTION) -from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION) +from homeassistant.components.meteo_france import ( + ATTRIBUTION, CONF_CITY, DATA_METEO_FRANCE, SENSOR_TYPES) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -27,19 +18,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): city = discovery_info[CONF_CITY] monitored_conditions = discovery_info[CONF_MONITORED_CONDITIONS] - client = hass.data[DATA_METEO_FRANCE][city] add_entities([MeteoFranceSensor(variable, client) - for variable in monitored_conditions], - True) + for variable in monitored_conditions], True) class MeteoFranceSensor(Entity): - """Representation of a Sensor.""" + """Representation of a Meteo-France sensor.""" def __init__(self, condition, client): - """Initialize the sensor.""" + """Initialize the Meteo-France sensor.""" self._condition = condition self._client = client self._state = None @@ -48,8 +37,8 @@ class MeteoFranceSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._data["name"], - SENSOR_TYPES[self._condition][0]) + return "{} {}".format( + self._data['name'], SENSOR_TYPES[self._condition][0]) @property def state(self): @@ -59,15 +48,11 @@ class MeteoFranceSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - if self._condition == 'next_rain' and "rain_forecast" in self._data: + if self._condition == 'next_rain' and 'rain_forecast' in self._data: return { - **{ - STATE_ATTR_FORECAST: self._data["rain_forecast"], - }, - ** self._data["next_rain_intervals"], - **{ - ATTR_ATTRIBUTION: ATTRIBUTION - } + **{STATE_ATTR_FORECAST: self._data['rain_forecast']}, + ** self._data['next_rain_intervals'], + **{ATTR_ATTRIBUTION: ATTRIBUTION} } return {ATTR_ATTRIBUTION: ATTRIBUTION} @@ -83,6 +68,6 @@ class MeteoFranceSensor(Entity): self._data = self._client.get_data() self._state = self._data[self._condition] except KeyError: - _LOGGER.error("No condition `%s` for location `%s`", - self._condition, self._data["name"]) + _LOGGER.error("No condition %s for location %s", + self._condition, self._data['name']) self._state = None diff --git a/homeassistant/components/weather/meteo_france.py b/homeassistant/components/meteo_france/weather.py similarity index 83% rename from homeassistant/components/weather/meteo_france.py rename to homeassistant/components/meteo_france/weather.py index cf8b2b497fb..849c9d9da10 100644 --- a/homeassistant/components/weather/meteo_france.py +++ b/homeassistant/components/meteo_france/weather.py @@ -1,4 +1,4 @@ -"""Support for Meteo france weather service.""" +"""Support for Meteo-France weather service.""" from datetime import datetime, timedelta import logging @@ -18,7 +18,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return city = discovery_info[CONF_CITY] - client = hass.data[DATA_METEO_FRANCE][city] add_entities([MeteoFranceWeather(client)], True) @@ -40,21 +39,21 @@ class MeteoFranceWeather(WeatherEntity): @property def name(self): """Return the name of the sensor.""" - return self._data["name"] + return self._data['name'] @property def condition(self): """Return the current condition.""" - return self.format_condition(self._data["weather"]) + return self.format_condition(self._data['weather']) @property def temperature(self): - """Return the platform temperature.""" - return self._data["temperature"] + """Return the temperature.""" + return self._data['temperature'] @property def humidity(self): - """Return the platform temperature.""" + """Return the humidity.""" return None @property @@ -65,12 +64,12 @@ class MeteoFranceWeather(WeatherEntity): @property def wind_speed(self): """Return the wind speed.""" - return self._data["wind_speed"] + return self._data['wind_speed'] @property def wind_bearing(self): """Return the wind bearing.""" - return self._data["wind_bearing"] + return self._data['wind_bearing'] @property def attribution(self): @@ -83,14 +82,14 @@ class MeteoFranceWeather(WeatherEntity): reftime = datetime.now().replace(hour=12, minute=00) reftime += timedelta(hours=24) forecast_data = [] - for key in self._data["forecast"]: - value = self._data["forecast"][key] + for key in self._data['forecast']: + value = self._data['forecast'][key] data_dict = { ATTR_FORECAST_TIME: reftime.isoformat(), ATTR_FORECAST_TEMP: int(value['max_temp']), ATTR_FORECAST_TEMP_LOW: int(value['min_temp']), ATTR_FORECAST_CONDITION: - self.format_condition(value["weather"]) + self.format_condition(value['weather']) } reftime = reftime + timedelta(hours=24) forecast_data.append(data_dict) From b44ff38f5a04a6237438a04b1976374ec0a5b140 Mon Sep 17 00:00:00 2001 From: Eliran Turgeman Date: Fri, 15 Feb 2019 12:48:27 +0200 Subject: [PATCH 014/253] Fix "Unable to find entity" at Waze component (#21087) Should fix https://github.com/home-assistant/home-assistant/issues/20953 (Unable to create this issue on my HA platform) --- homeassistant/components/sensor/waze_travel_time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index 6634339be4b..8ed4f50a9c7 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -70,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor = WazeTravelTime(name, origin, destination, region, incl_filter, excl_filter, realtime) - add_entities([sensor], True) + add_entities([sensor]) # Wait until start event is sent to load this component. hass.bus.listen_once( From f3786e2f2b097713fc6e4f7820a44773614c6eb6 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 15 Feb 2019 12:19:42 +0100 Subject: [PATCH 015/253] Point alarm control (#20972) * initial working example of alarm_control * fixes for alarm_control * arm home is the same as arm away * updated documentation * final fixes * pypoint version up * fixes for Martin --- homeassistant/components/point/__init__.py | 21 +++++- .../components/point/alarm_control_panel.py | 74 +++++++++++++++++++ requirements_all.txt | 2 +- 3 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/point/alarm_control_panel.py diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index f223ded998f..dc839756469 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -20,7 +20,7 @@ from .const import ( CONF_WEBHOOK_URL, DOMAIN, EVENT_RECEIVED, POINT_DISCOVERY_NEW, SCAN_INTERVAL, SIGNAL_UPDATE_ENTITY, SIGNAL_WEBHOOK) -REQUIREMENTS = ['pypoint==1.0.8'] +REQUIREMENTS = ['pypoint==1.1.1'] _LOGGER = logging.getLogger(__name__) @@ -159,6 +159,7 @@ class MinutPointClient(): session): """Initialize the Minut data object.""" self._known_devices = set() + self._known_homes = set() self._hass = hass self._config_entry = config_entry self._is_available = True @@ -194,6 +195,10 @@ class MinutPointClient(): device_id) self._is_available = True + for home_id in self._client.homes: + if home_id not in self._known_homes: + await new_device(home_id, 'alarm_control_panel') + self._known_homes.add(home_id) for device in self._client.devices: if device.device_id not in self._known_devices: for component in ('sensor', 'binary_sensor'): @@ -213,6 +218,19 @@ class MinutPointClient(): """Remove the session webhook.""" return self._client.remove_webhook() + @property + def homes(self): + """Return known homes.""" + return self._client.homes + + def alarm_disarm(self, home_id): + """Send alarm disarm command.""" + return self._client.alarm_disarm(home_id) + + def alarm_arm(self, home_id): + """Send alarm arm command.""" + return self._client.alarm_arm(home_id) + class MinutPointEntity(Entity): """Base Entity used by the sensors.""" @@ -286,6 +304,7 @@ class MinutPointEntity(Entity): 'model': 'Point v{}'.format(device['hardware_version']), 'name': device['description'], 'sw_version': device['firmware']['installed'], + 'via_hub': (DOMAIN, device['home']), } @property diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py new file mode 100644 index 00000000000..10387fdc7c4 --- /dev/null +++ b/homeassistant/components/point/alarm_control_panel.py @@ -0,0 +1,74 @@ +"""Support for Minut Point.""" +import logging + +from homeassistant.components.alarm_control_panel import (DOMAIN, + AlarmControlPanel) +from homeassistant.const import (STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED) +from homeassistant.components.point.const import ( + DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW) +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a Point's alarm_control_panel based on a config entry.""" + async def async_discover_home(home_id): + """Discover and add a discovered home.""" + client = hass.data[POINT_DOMAIN][config_entry.entry_id] + async_add_entities([MinutPointAlarmControl(client, home_id)], True) + + async_dispatcher_connect( + hass, POINT_DISCOVERY_NEW.format(DOMAIN, POINT_DOMAIN), + async_discover_home) + + +class MinutPointAlarmControl(AlarmControlPanel): + """The platform class required by Home Assistant.""" + + def __init__(self, point_client, home_id): + """Initialize the entity.""" + self._client = point_client + self._home_id = home_id + + @property + def _home(self): + """Return the home object.""" + return self._client.homes[self._home_id] + + @property + def name(self): + """Return name of the device.""" + return self._home['name'] + + @property + def state(self): + """Return state of the device.""" + return STATE_ALARM_DISARMED if self._home[ + 'alarm_status'] == 'off' else STATE_ALARM_ARMED_AWAY + + def alarm_disarm(self, code=None): + """Send disarm command.""" + status = self._client.alarm_disarm(self._home_id) + if status: + self._home['alarm_status'] = 'off' + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + status = self._client.alarm_arm(self._home_id) + if status: + self._home['alarm_status'] = 'on' + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return 'point.{}'.format(self._home_id) + + @property + def device_info(self): + """Return a device description for device registry.""" + return { + 'identifiers': {(POINT_DOMAIN, self._home_id)}, + 'name': self.name, + 'manufacturer': 'Minut', + } diff --git a/requirements_all.txt b/requirements_all.txt index 592afdd634f..9061348a41c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1196,7 +1196,7 @@ pypck==0.5.9 pypjlink2==1.2.0 # homeassistant.components.point -pypoint==1.0.8 +pypoint==1.1.1 # homeassistant.components.sensor.pollen pypollencom==2.2.2 From 7b19428279ff2d563a21b69f06186361284bcfd5 Mon Sep 17 00:00:00 2001 From: Klaudiusz Staniek Date: Fri, 15 Feb 2019 14:13:44 +0100 Subject: [PATCH 016/253] Times of The Day Binary Sensor (#20068) * First commit * Times of the Day binary sensor added * Python 3.5 fixes and logging removed * Code refactored according to reviewer's suggestions * Fixed config template with friendly name support * Finall pep8 fixes * Removed async_generate_entity_id and moved initial calculation to async_added_to_hass * Change the configuration schema to follow the current requirements * Update according latest suggestsion * Fix typos and minor changes * Fix lint issue --- homeassistant/components/binary_sensor/tod.py | 217 +++++ tests/components/binary_sensor/test_tod.py | 839 ++++++++++++++++++ 2 files changed, 1056 insertions(+) create mode 100644 homeassistant/components/binary_sensor/tod.py create mode 100644 tests/components/binary_sensor/test_tod.py diff --git a/homeassistant/components/binary_sensor/tod.py b/homeassistant/components/binary_sensor/tod.py new file mode 100644 index 00000000000..7dc6e5ebe81 --- /dev/null +++ b/homeassistant/components/binary_sensor/tod.py @@ -0,0 +1,217 @@ +"""Support for representing current time of the day as binary sensors.""" +from datetime import datetime, timedelta +import logging + +import pytz +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.const import ( + CONF_AFTER, CONF_BEFORE, CONF_NAME, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.sun import ( + get_astral_event_date, get_astral_event_next) +from homeassistant.util import dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +ATTR_AFTER = 'after' +ATTR_BEFORE = 'before' +ATTR_NEXT_UPDATE = 'next_update' + +CONF_AFTER_OFFSET = 'after_offset' +CONF_BEFORE_OFFSET = 'before_offset' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_AFTER): + vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), + vol.Required(CONF_BEFORE): + vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_period, + vol.Optional(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_period, +}) + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Set up the ToD sensors.""" + if hass.config.time_zone is None: + _LOGGER.error("Timezone is not set in Home Assistant configuration") + return + + after = config[CONF_AFTER] + after_offset = config[CONF_AFTER_OFFSET] + before = config[CONF_BEFORE] + before_offset = config[CONF_BEFORE_OFFSET] + name = config[CONF_NAME] + sensor = TodSensor(name, after, after_offset, before, before_offset) + + async_add_entities([sensor]) + + +def is_sun_event(event): + """Return true if event is sun event not time.""" + return event in (SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) + + +class TodSensor(BinarySensorDevice): + """Time of the Day Sensor.""" + + def __init__(self, name, after, after_offset, before, before_offset): + """Init the ToD Sensor...""" + self._name = name + self._time_before = self._time_after = self._next_update = None + self._after_offset = after_offset + self._before_offset = before_offset + self._before = before + self._after = after + + @property + def should_poll(self): + """Sensor does not need to be polled.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def after(self): + """Return the timestamp for the begining of the period.""" + return self._time_after + + @property + def before(self): + """Return the timestamp for the end of the period.""" + return self._time_before + + @property + def is_on(self): + """Return True is sensor is on.""" + if self.after < self.before: + return self.after <= self.current_datetime < self.before + return False + + @property + def current_datetime(self): + """Return local current datetime according to hass configuration.""" + return dt_util.utcnow() + + @property + def next_update(self): + """Return the next update point in the UTC time.""" + return self._next_update + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return { + ATTR_AFTER: self.after.astimezone( + self.hass.config.time_zone).isoformat(), + ATTR_BEFORE: self.before.astimezone( + self.hass.config.time_zone).isoformat(), + ATTR_NEXT_UPDATE: self.next_update.astimezone( + self.hass.config.time_zone).isoformat(), + } + + def _calculate_initial_boudary_time(self): + """Calculate internal absolute time boudaries.""" + nowutc = self.current_datetime + # If after value is a sun event instead of absolute time + if is_sun_event(self._after): + # Calculate the today's event utc time or + # if not available take next + after_event_date = \ + get_astral_event_date(self.hass, self._after, nowutc) or \ + get_astral_event_next(self.hass, self._after, nowutc) + else: + # Convert local time provided to UTC today + # datetime.combine(date, time, tzinfo) is not supported + # in python 3.5. The self._after is provided + # with hass configured TZ not system wide + after_event_date = datetime.combine( + nowutc, self._after.replace( + tzinfo=self.hass.config.time_zone)).astimezone(tz=pytz.UTC) + + self._time_after = after_event_date + + # If before value is a sun event instead of absolute time + if is_sun_event(self._before): + # Calculate the today's event utc time or if not available take + # next + before_event_date = \ + get_astral_event_date(self.hass, self._before, nowutc) or \ + get_astral_event_next(self.hass, self._before, nowutc) + # Before is earlier than after + if before_event_date < after_event_date: + # Take next day for before + before_event_date = get_astral_event_next( + self.hass, self._before, after_event_date) + else: + # Convert local time provided to UTC today, see above + before_event_date = datetime.combine( + nowutc, self._before.replace( + tzinfo=self.hass.config.time_zone)).astimezone(tz=pytz.UTC) + + # It is safe to add timedelta days=1 to UTC as there is no DST + if before_event_date < after_event_date + self._after_offset: + before_event_date += timedelta(days=1) + + self._time_before = before_event_date + + # Add offset to utc boundaries according to the configuration + self._time_after += self._after_offset + self._time_before += self._before_offset + + def _turn_to_next_day(self): + """Turn to to the next day.""" + if is_sun_event(self._after): + self._time_after = get_astral_event_next( + self.hass, self._after, + self._time_after - self._after_offset) + self._time_after += self._after_offset + else: + # Offset is already there + self._time_after += timedelta(days=1) + + if is_sun_event(self._before): + self._time_before = get_astral_event_next( + self.hass, self._before, + self._time_before - self._before_offset) + self._time_before += self._before_offset + else: + # Offset is already there + self._time_before += timedelta(days=1) + + async def async_added_to_hass(self): + """Call when entity about to be added to Home Assistant.""" + await super().async_added_to_hass() + self._calculate_initial_boudary_time() + self._calculate_next_update() + self._point_in_time_listener(dt_util.now()) + + def _calculate_next_update(self): + """Datetime when the next update to the state.""" + now = self.current_datetime + if now < self.after: + self._next_update = self.after + return + if now < self.before: + self._next_update = self.before + return + self._turn_to_next_day() + self._next_update = self.after + + @callback + def _point_in_time_listener(self, now): + """Run when the state of the sensor should be updated.""" + self._calculate_next_update() + self.async_schedule_update_ha_state() + + async_track_point_in_utc_time( + self.hass, self._point_in_time_listener, self.next_update) diff --git a/tests/components/binary_sensor/test_tod.py b/tests/components/binary_sensor/test_tod.py new file mode 100644 index 00000000000..3c083141962 --- /dev/null +++ b/tests/components/binary_sensor/test_tod.py @@ -0,0 +1,839 @@ +"""Test Times of the Day Binary Sensor.""" +import unittest +from unittest.mock import patch +from datetime import timedelta, datetime +import pytz + +from homeassistant import setup +import homeassistant.core as ha +from homeassistant.const import STATE_OFF, STATE_ON +import homeassistant.util.dt as dt_util +from homeassistant.setup import setup_component +from tests.common import ( + get_test_home_assistant, assert_setup_component) +from homeassistant.helpers.sun import ( + get_astral_event_date, get_astral_event_next) + + +class TestBinarySensorTod(unittest.TestCase): + """Test for Binary sensor tod platform.""" + + hass = None + # pylint: disable=invalid-name + + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.latitute = 50.27583 + self.hass.config.longitude = 18.98583 + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup(self): + """Test the setup.""" + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Early Morning', + 'after': 'sunrise', + 'after_offset': '-02:00', + 'before': '7:00', + 'before_offset': '1:00' + }, + { + 'platform': 'tod', + 'name': 'Morning', + 'after': 'sunrise', + 'before': '12:00' + } + ], + } + with assert_setup_component(2): + assert setup.setup_component( + self.hass, 'binary_sensor', config) + + def test_setup_no_sensors(self): + """Test setup with no sensors.""" + with assert_setup_component(0): + assert setup.setup_component(self.hass, 'binary_sensor', { + 'binary_sensor': { + 'platform': 'tod' + } + }) + + def test_in_period_on_start(self): + """Test simple setting.""" + test_time = datetime( + 2019, 1, 10, 18, 43, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Evening', + 'after': '18:00', + 'before': '22:00' + } + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.evening') + assert state.state == STATE_ON + + def test_midnight_turnover_before_midnight_inside_period(self): + """Test midnight turnover setting before midnight inside period .""" + test_time = datetime( + 2019, 1, 10, 22, 30, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': '22:00', + 'before': '5:00' + }, + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_ON + + def test_midnight_turnover_after_midnight_inside_period(self): + """Test midnight turnover setting before midnight inside period .""" + test_time = datetime( + 2019, 1, 10, 21, 00, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': '22:00', + 'before': '5:00' + }, + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_OFF + + self.hass.block_till_done() + + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time + timedelta(hours=1)): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: test_time + timedelta(hours=1)}) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_ON + + def test_midnight_turnover_before_midnight_outside_period(self): + """Test midnight turnover setting before midnight outside period.""" + test_time = datetime( + 2019, 1, 10, 20, 30, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': '22:00', + 'before': '5:00' + } + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_OFF + + def test_midnight_turnover_after_midnight_outside_period(self): + """Test midnight turnover setting before midnight inside period .""" + test_time = datetime( + 2019, 1, 10, 20, 0, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': '22:00', + 'before': '5:00' + } + ] + } + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=test_time): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_OFF + + switchover_time = datetime( + 2019, 1, 11, 4, 59, 0, tzinfo=self.hass.config.time_zone) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=switchover_time): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: switchover_time}) + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_ON + + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=switchover_time + timedelta( + minutes=1, seconds=1)): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: switchover_time + timedelta( + minutes=1, seconds=1)}) + + self.hass.block_till_done() + state = self.hass.states.get('binary_sensor.night') + assert state.state == STATE_OFF + + def test_from_sunrise_to_sunset(self): + """Test period from sunrise to sunset.""" + test_time = datetime( + 2019, 1, 12, tzinfo=self.hass.config.time_zone) + sunrise = dt_util.as_local(get_astral_event_date( + self.hass, 'sunrise', dt_util.as_utc(test_time))) + sunset = dt_util.as_local(get_astral_event_date( + self.hass, 'sunset', dt_util.as_utc(test_time))) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': 'sunrise', + 'before': 'sunset' + } + ] + } + entity_id = 'binary_sensor.day' + testtime = sunrise + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_from_sunset_to_sunrise(self): + """Test period from sunset to sunrise.""" + test_time = datetime( + 2019, 1, 12, tzinfo=self.hass.config.time_zone) + sunset = dt_util.as_local(get_astral_event_date( + self.hass, 'sunset', test_time)) + sunrise = dt_util.as_local(get_astral_event_next( + self.hass, 'sunrise', sunset)) + # assert sunset == sunrise + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Night', + 'after': 'sunset', + 'before': 'sunrise' + } + ] + } + entity_id = 'binary_sensor.night' + testtime = sunset + timedelta(minutes=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunset + timedelta(minutes=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(minutes=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + self.hass.block_till_done() + # assert state == "dupa" + assert state.state == STATE_OFF + + testtime = sunrise + timedelta(minutes=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_offset(self): + """Test offset.""" + after = datetime( + 2019, 1, 10, 18, 0, 0, + tzinfo=self.hass.config.time_zone) + \ + timedelta(hours=1, minutes=34) + before = datetime( + 2019, 1, 10, 22, 0, 0, + tzinfo=self.hass.config.time_zone) + \ + timedelta(hours=1, minutes=45) + entity_id = 'binary_sensor.evening' + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Evening', + 'after': '18:00', + 'after_offset': '1:34', + 'before': '22:00', + 'before_offset': '1:45' + } + ] + } + testtime = after + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = after + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = before + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = before + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = before + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_offset_overnight(self): + """Test offset overnight.""" + after = datetime( + 2019, 1, 10, 18, 0, 0, + tzinfo=self.hass.config.time_zone) + \ + timedelta(hours=1, minutes=34) + entity_id = 'binary_sensor.evening' + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Evening', + 'after': '18:00', + 'after_offset': '1:34', + 'before': '22:00', + 'before_offset': '3:00' + } + ] + } + testtime = after + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = after + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + def test_norwegian_case_winter(self): + """Test location in Norway where the sun doesn't set in summer.""" + self.hass.config.latitude = 69.6 + self.hass.config.longitude = 18.8 + + test_time = datetime(2010, 1, 1, tzinfo=self.hass.config.time_zone) + sunrise = dt_util.as_local(get_astral_event_next( + self.hass, 'sunrise', dt_util.as_utc(test_time))) + sunset = dt_util.as_local(get_astral_event_next( + self.hass, 'sunset', dt_util.as_utc(test_time))) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': 'sunrise', + 'before': 'sunset' + } + ] + } + entity_id = 'binary_sensor.day' + testtime = test_time + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_norwegian_case_summer(self): + """Test location in Norway where the sun doesn't set in summer.""" + self.hass.config.latitude = 69.6 + self.hass.config.longitude = 18.8 + + test_time = datetime(2010, 6, 1, tzinfo=self.hass.config.time_zone) + sunrise = dt_util.as_local(get_astral_event_next( + self.hass, 'sunrise', dt_util.as_utc(test_time))) + sunset = dt_util.as_local(get_astral_event_next( + self.hass, 'sunset', dt_util.as_utc(test_time))) + print(sunrise) + print(sunset) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': 'sunrise', + 'before': 'sunset' + } + ] + } + entity_id = 'binary_sensor.day' + testtime = test_time + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + def test_sun_offset(self): + """Test sun event with offset.""" + test_time = datetime( + 2019, 1, 12, tzinfo=self.hass.config.time_zone) + sunrise = dt_util.as_local(get_astral_event_date( + self.hass, 'sunrise', dt_util.as_utc(test_time)) + + timedelta(hours=-1, minutes=-30)) + sunset = dt_util.as_local(get_astral_event_date( + self.hass, 'sunset', dt_util.as_utc(test_time)) + + timedelta(hours=1, minutes=30)) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': 'sunrise', + 'after_offset': '-1:30', + 'before': 'sunset', + 'before_offset': '1:30' + } + ] + } + entity_id = 'binary_sensor.day' + testtime = sunrise + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + testtime = sunrise + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=-1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + self.hass.block_till_done() + + testtime = sunset + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + self.hass.block_till_done() + + testtime = sunset + timedelta(seconds=1) + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_OFF + + test_time = test_time + timedelta(days=1) + sunrise = dt_util.as_local(get_astral_event_date( + self.hass, 'sunrise', dt_util.as_utc(test_time)) + + timedelta(hours=-1, minutes=-30)) + testtime = sunrise + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, { + ha.ATTR_NOW: testtime}) + self.hass.block_till_done() + + state = self.hass.states.get(entity_id) + assert state.state == STATE_ON + + def test_dst(self): + """Test sun event with offset.""" + self.hass.config.time_zone = pytz.timezone('CET') + test_time = datetime( + 2019, 3, 30, 3, 0, 0, tzinfo=self.hass.config.time_zone) + config = { + 'binary_sensor': [ + { + 'platform': 'tod', + 'name': 'Day', + 'after': '2:30', + 'before': '2:40' + } + ] + } + # after 2019-03-30 03:00 CET the next update should ge scheduled + # at 3:30 not 2:30 local time + # Internally the + entity_id = 'binary_sensor.day' + testtime = test_time + with patch('homeassistant.components.binary_sensor.tod.dt_util.utcnow', + return_value=testtime): + setup_component(self.hass, 'binary_sensor', config) + + self.hass.block_till_done() + state = self.hass.states.get(entity_id) + state.attributes['after'] == '2019-03-31T03:30:00+02:00' + state.attributes['before'] == '2019-03-31T03:40:00+02:00' + state.attributes['next_update'] == '2019-03-31T03:30:00+02:00' + assert state.state == STATE_OFF From 656d39e3ec9d20d90b071de5717fcacb71649c01 Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Fri, 15 Feb 2019 13:35:26 +0000 Subject: [PATCH 017/253] Nissan Leaf Integration (Carwings / NissanConnect EV) (#19786) * Added work so far. * Change interval so nobody drains their battery when I put this online * Added the warning notice. * Async setup * Still broken, but we're getting there. * Back to synchronous, moved refresh stuff into DataStore * Functional sensors! * Added working switches, tweaked intervals a bit * Fixed turn off result * Moved plug status to binary_sensor, added smart intervals * Documentation and car nickname stuff * Syntax fixes and coveragerc additions * Style fixes * Fixing the final line length * Fixed an issue with newer models and bad climate data * Forgot to check my line endings. * New icons for most of the components * Hotfix for handling Nissan's awful servers * Merge in fixes made by Phil Cole Remove invalid FIXMEs and update TODOs Fixes for pylint and test for CarwingsError exception rather than Exception Flake8 fixes Add pycarwings2 to requirements_all.txt Add extra configuration documentation. Use pycarwings2 from pip. Check server dates between requests. Add sensor device class for battery. Async conversion fixes flake8 fixes and docstrings Non-async charging is OK Handle multiple cars in the configuration Convert to async. Better imports for platforms Fix scanning interval & prevent extra refreshes. async switchover Check discovery_info to prevent load of platforms Ensure update frequency is always above a minimum interval (1 min). Platforms don't have return values Use values() instead of items() when not using key Use snake_case (LeafCore becomes leaf_core) commit 418b6bbcc49cf2909aac85869440435410abf3fd * Add pycarwings2 to requirements_all.txt * Make stopping charge error an 'info'. Remove TODO. * Request update from car after sending start charging command. * Delay initial (slow) update for 15 seconds and make async * Flake8 line length fixes * Try to fix D401 'imperative mood' git diff tox errors * Try to fix more D401 'imperative mood' tox errors * Default interval of an hour in code, to match comments. * Update to pycarwings2 2.3 * Update to pycarwings2 2.3 in requirements_all.txt * Remove documentation, instead refering to home-assistant.io * Remove unneeded dispatcher_send() * Remove unneeded requirements comments * Combine excess debugging. * Remove single line method signal_components() * Bump to version 2.4 of pycarwings2 * Remove unused dispatcher_send * Simplify logging of LeafEntity registration * Update requirements_all.txt * Multiple changes Increase timeout to 30 seconds Only consider battery_status Fix plugged in status Better attempts at try/exception handling * Fix line length * Use pycarwings 2.5 * Remove pointless 'is True' * Remove unnecessary 'is True/False' * Remove unnecessary 'is True/False' * Use LENGTH_MILES and LENGTH_KILOMETERS * Remove excess logging in setup_platform() * Remove unnecessary 'is True' * Use pycarwings2 version 2.6 * Require pycarwings2 version 2.7. * Increase sleep delay for climate and location reponses. * Remove unnecessary 'is True' * Increase frequent polling warning to _LOGGER.warning() * Use DEVICE_CLASS_BATTERY * Remove extraneous 'is True'. * Move icon strings to constants. * Remove unneeded key. * LeafRangeSensor ac_on property is internal. * Flake8 missing line * Remove homebridge attributes. * Remove round battery % and range to whole numbers * Use pycarwings2 2.8 * Move to embedded component model * Reduce maximum attempts to 10 (5 mins) * Include attempt count in 'waiting' log message * Use await instead of yield. Remove @asyncio.coroutine decorators. * Add @filcole as nissan_leaf codeowner * Fix checking for if not data returned from vehicle. Don't double send signal on location update. * Exposed updated_on, update_in_progress and next_update attributes. * Add nissan_leaf.update service that triggers an update. * Flake8 line fixes * Remove excess and double logging. * Add updated_on attribute for device tracker. * Fix crash if pycarwings2 doesn't provide cruising ranges. * Minor changes * Minor changes * Minor changes * Minor changes * Minor changes --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/nissan_leaf/__init__.py | 514 ++++++++++++++++++ .../components/nissan_leaf/binary_sensor.py | 44 ++ .../components/nissan_leaf/device_tracker.py | 46 ++ .../components/nissan_leaf/sensor.py | 113 ++++ .../components/nissan_leaf/switch.py | 99 ++++ requirements_all.txt | 3 + 8 files changed, 821 insertions(+) create mode 100644 homeassistant/components/nissan_leaf/__init__.py create mode 100644 homeassistant/components/nissan_leaf/binary_sensor.py create mode 100644 homeassistant/components/nissan_leaf/device_tracker.py create mode 100644 homeassistant/components/nissan_leaf/sensor.py create mode 100644 homeassistant/components/nissan_leaf/switch.py diff --git a/.coveragerc b/.coveragerc index 4e06eaaef29..dec0594e7a3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -327,6 +327,7 @@ omit = homeassistant/components/nest/* homeassistant/components/netatmo/* homeassistant/components/netgear_lte/* + homeassistant/components/nissan_leaf/* homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sqs.py diff --git a/CODEOWNERS b/CODEOWNERS index 64263598121..fc3ba96097c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -224,6 +224,7 @@ homeassistant/components/*/mystrom.py @fabaff # N homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/*/ness_alarm.py @nickw444 +homeassistant/components/nissan_leaf/* @filcole # O homeassistant/components/openuv/* @bachya diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py new file mode 100644 index 00000000000..f5a8217242d --- /dev/null +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -0,0 +1,514 @@ +"""Support for the Nissan Leaf Carwings/Nissan Connect API.""" +import asyncio +from datetime import datetime, timedelta +import logging +import sys +import urllib + +import voluptuous as vol + +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow + +REQUIREMENTS = ['pycarwings2==2.8'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'nissan_leaf' +DATA_LEAF = 'nissan_leaf_data' + +DATA_BATTERY = 'battery' +DATA_LOCATION = 'location' +DATA_CHARGING = 'charging' +DATA_PLUGGED_IN = 'plugged_in' +DATA_CLIMATE = 'climate' +DATA_RANGE_AC = 'range_ac_on' +DATA_RANGE_AC_OFF = 'range_ac_off' + +CONF_NCONNECT = 'nissan_connect' +CONF_INTERVAL = 'update_interval' +CONF_CHARGING_INTERVAL = 'update_interval_charging' +CONF_CLIMATE_INTERVAL = 'update_interval_climate' +CONF_REGION = 'region' +CONF_VALID_REGIONS = ['NNA', 'NE', 'NCI', 'NMA', 'NML'] +CONF_FORCE_MILES = 'force_miles' + +INITIAL_UPDATE = timedelta(seconds=15) +MIN_UPDATE_INTERVAL = timedelta(minutes=2) +DEFAULT_INTERVAL = timedelta(hours=1) +DEFAULT_CHARGING_INTERVAL = timedelta(minutes=15) +DEFAULT_CLIMATE_INTERVAL = timedelta(minutes=5) +RESTRICTED_BATTERY = 2 +RESTRICTED_INTERVAL = timedelta(hours=12) + +MAX_RESPONSE_ATTEMPTS = 10 + +PYCARWINGS2_SLEEP = 30 + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_REGION): vol.In(CONF_VALID_REGIONS), + vol.Optional(CONF_NCONNECT, default=True): cv.boolean, + vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): ( + vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))), + vol.Optional(CONF_CHARGING_INTERVAL, + default=DEFAULT_CHARGING_INTERVAL): ( + vol.All(cv.time_period, + vol.Clamp(min=MIN_UPDATE_INTERVAL))), + vol.Optional(CONF_CLIMATE_INTERVAL, + default=DEFAULT_CLIMATE_INTERVAL): ( + vol.All(cv.time_period, + vol.Clamp(min=MIN_UPDATE_INTERVAL))), + vol.Optional(CONF_FORCE_MILES, default=False): cv.boolean + })]) +}, extra=vol.ALLOW_EXTRA) + +LEAF_COMPONENTS = [ + 'sensor', 'switch', 'binary_sensor', 'device_tracker' +] + +SIGNAL_UPDATE_LEAF = 'nissan_leaf_update' + +SERVICE_UPDATE_LEAF = 'update' +ATTR_VIN = 'vin' + +UPDATE_LEAF_SCHEMA = vol.Schema({ + vol.Required(ATTR_VIN): cv.string, +}) + + +async def async_setup(hass, config): + """Set up the Nissan Leaf component.""" + import pycarwings2 + + async def handle_update(service): + # It would be better if this was changed to use nickname, or + # an entity name rather than a vin. + vin = service.data.get(ATTR_VIN, '') + + if vin in hass.data[DATA_LEAF]: + data_store = hass.data[DATA_LEAF][vin] + async_track_point_in_utc_time( + hass, data_store.async_update_data, utcnow()) + return True + + _LOGGER.debug("Vin %s not recognised for update", vin) + return False + + async def async_setup_leaf(car_config): + """Set up a car.""" + _LOGGER.debug("Logging into You+Nissan...") + + username = car_config[CONF_USERNAME] + password = car_config[CONF_PASSWORD] + region = car_config[CONF_REGION] + leaf = None + + async def leaf_login(): + nonlocal leaf + sess = pycarwings2.Session(username, password, region) + leaf = sess.get_leaf() + + try: + # This might need to be made async (somehow) causes + # homeassistant to be slow to start + await hass.async_add_job(leaf_login) + except(RuntimeError, urllib.error.HTTPError): + _LOGGER.error( + "Unable to connect to Nissan Connect with " + "username and password") + return False + except KeyError: + _LOGGER.error( + "Unable to fetch car details..." + " do you actually have a Leaf connected to your account?") + return False + except pycarwings2.CarwingsError: + _LOGGER.error( + "An unknown error occurred while connecting to Nissan: %s", + sys.exc_info()[0]) + return False + + _LOGGER.warning( + "WARNING: This may poll your Leaf too often, and drain the 12V" + " battery. If you drain your cars 12V battery it WILL NOT START" + " as the drive train battery won't connect." + " Don't set the intervals too low.") + + data_store = LeafDataStore(leaf, hass, car_config) + hass.data[DATA_LEAF][leaf.vin] = data_store + + for component in LEAF_COMPONENTS: + if component != 'device_tracker' or car_config[CONF_NCONNECT]: + load_platform(hass, component, DOMAIN, {}, car_config) + + async_track_point_in_utc_time(hass, data_store.async_update_data, + utcnow() + INITIAL_UPDATE) + + hass.data[DATA_LEAF] = {} + tasks = [async_setup_leaf(car) for car in config[DOMAIN]] + if tasks: + await asyncio.wait(tasks, loop=hass.loop) + + hass.services.async_register(DOMAIN, SERVICE_UPDATE_LEAF, handle_update, + schema=UPDATE_LEAF_SCHEMA) + + return True + + +class LeafDataStore: + """Nissan Leaf Data Store.""" + + def __init__(self, leaf, hass, car_config): + """Initialise the data store.""" + self.leaf = leaf + self.car_config = car_config + self.nissan_connect = car_config[CONF_NCONNECT] + self.force_miles = car_config[CONF_FORCE_MILES] + self.hass = hass + self.data = {} + self.data[DATA_CLIMATE] = False + self.data[DATA_BATTERY] = 0 + self.data[DATA_CHARGING] = False + self.data[DATA_LOCATION] = False + self.data[DATA_RANGE_AC] = 0 + self.data[DATA_RANGE_AC_OFF] = 0 + self.data[DATA_PLUGGED_IN] = False + self.next_update = None + self.last_check = None + self.request_in_progress = False + # Timestamp of last successful response from battery, + # climate or location. + self.last_battery_response = None + self.last_climate_response = None + self.last_location_response = None + self._remove_listener = None + + async def async_update_data(self, now): + """Update data from nissan leaf.""" + # Prevent against a previously scheduled update and an ad-hoc update + # started from an update from both being triggered. + if self._remove_listener: + self._remove_listener() + self._remove_listener = None + + # Clear next update whilst this update is underway + self.next_update = None + + await self.async_refresh_data(now) + self.next_update = self.get_next_interval() + _LOGGER.debug("Next update=%s", self.next_update) + self._remove_listener = async_track_point_in_utc_time( + self.hass, self.async_update_data, self.next_update) + + def get_next_interval(self): + """Calculate when the next update should occur.""" + base_interval = self.car_config[CONF_INTERVAL] + climate_interval = self.car_config[CONF_CLIMATE_INTERVAL] + charging_interval = self.car_config[CONF_CHARGING_INTERVAL] + + # The 12V battery is used when communicating with Nissan servers. + # The 12V battery is charged from the traction battery when not + # connected and when the traction battery has enough charge. To + # avoid draining the 12V battery we shall restrict the update + # frequency if low battery detected. + if (self.last_battery_response is not None and + self.data[DATA_CHARGING] is False and + self.data[DATA_BATTERY] <= RESTRICTED_BATTERY): + _LOGGER.info("Low battery so restricting refresh frequency (%s)", + self.leaf.nickname) + interval = RESTRICTED_INTERVAL + else: + intervals = [base_interval] + _LOGGER.debug("Could use base interval=%s", base_interval) + + if self.data[DATA_CHARGING]: + intervals.append(charging_interval) + _LOGGER.debug("Could use charging interval=%s", + charging_interval) + + if self.data[DATA_CLIMATE]: + intervals.append(climate_interval) + _LOGGER.debug( + "Could use climate interval=%s", climate_interval) + + interval = min(intervals) + _LOGGER.debug("Resulting interval=%s", interval) + + return utcnow() + interval + + async def async_refresh_data(self, now): + """Refresh the leaf data and update the datastore.""" + from pycarwings2 import CarwingsError + + if self.request_in_progress: + _LOGGER.debug("Refresh currently in progress for %s", + self.leaf.nickname) + return + + _LOGGER.debug("Updating Nissan Leaf Data") + + self.last_check = datetime.today() + self.request_in_progress = True + + server_response = await self.async_get_battery() + + if server_response is not None: + _LOGGER.debug("Server Response: %s", server_response.__dict__) + + if server_response.answer['status'] == 200: + self.data[DATA_BATTERY] = server_response.battery_percent + + # pycarwings2 library doesn't always provide cruising rnages + # so we have to check if they exist before we can use them. + # Root cause: the nissan servers don't always send the data. + if hasattr(server_response, 'cruising_range_ac_on_km'): + self.data[DATA_RANGE_AC] = ( + server_response.cruising_range_ac_on_km + ) + else: + self.data[DATA_RANGE_AC] = None + + if hasattr(server_response, 'cruising_range_ac_off_km'): + self.data[DATA_RANGE_AC_OFF] = ( + server_response.cruising_range_ac_off_km + ) + else: + self.data[DATA_RANGE_AC_OFF] = None + + self.data[DATA_PLUGGED_IN] = ( + server_response.is_connected + ) + async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) + self.last_battery_response = utcnow() + + # Climate response only updated if battery data updated first. + if server_response is not None: + try: + climate_response = await self.async_get_climate() + if climate_response is not None: + _LOGGER.debug("Got climate data for Leaf: %s", + climate_response.__dict__) + self.data[DATA_CLIMATE] = climate_response.is_hvac_running + self.last_climate_response = utcnow() + except CarwingsError: + _LOGGER.error("Error fetching climate info") + + if self.nissan_connect: + try: + location_response = await self.async_get_location() + + if location_response is None: + _LOGGER.debug("Empty Location Response Received") + self.data[DATA_LOCATION] = None + else: + _LOGGER.debug("Got location data for Leaf") + self.data[DATA_LOCATION] = location_response + self.last_location_response = utcnow() + + _LOGGER.debug("Location Response: %s", + location_response.__dict__) + except CarwingsError: + _LOGGER.error("Error fetching location info") + + self.request_in_progress = False + async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) + + @staticmethod + def _extract_start_date(battery_info): + """Extract the server date from the battery response.""" + try: + return battery_info.answer[ + "BatteryStatusRecords"]["OperationDateAndTime"] + except KeyError: + return None + + async def async_get_battery(self): + """Request battery update from Nissan servers.""" + from pycarwings2 import CarwingsError + try: + # First, check nissan servers for the latest data + start_server_info = await self.hass.async_add_job( + self.leaf.get_latest_battery_status + ) + + # Store the date from the nissan servers + start_date = self._extract_start_date(start_server_info) + if start_date is None: + _LOGGER.info("No start date from servers. Aborting") + return None + + _LOGGER.info("Start server date=%s", start_date) + + # Request battery update from the car + _LOGGER.info("Requesting battery update, %s", self.leaf.vin) + request = await self.hass.async_add_job(self.leaf.request_update) + if not request: + _LOGGER.error("Battery update request failed") + return None + + for attempt in range(MAX_RESPONSE_ATTEMPTS): + _LOGGER.info("Waiting %s seconds for battery update (%s) (%s)", + PYCARWINGS2_SLEEP, self.leaf.vin, attempt) + await asyncio.sleep(PYCARWINGS2_SLEEP) + + # Note leaf.get_status_from_update is always returning 0, so + # don't try to use it anymore. + server_info = await self.hass.async_add_job( + self.leaf.get_latest_battery_status + ) + + latest_date = self._extract_start_date(server_info) + _LOGGER.info("Latest server date=%s", latest_date) + if latest_date is not None and latest_date != start_date: + return server_info + + _LOGGER.info("%s attempts exceeded return latest data from server", + MAX_RESPONSE_ATTEMPTS) + return server_info + except CarwingsError: + _LOGGER.error("An error occurred getting battery status.") + return None + + async def async_get_climate(self): + """Request climate data from Nissan servers.""" + from pycarwings2 import CarwingsError + try: + request = await self.hass.async_add_job( + self.leaf.get_latest_hvac_status + ) + return request + except CarwingsError: + _LOGGER.error( + "An error occurred communicating with the car %s", + self.leaf.vin) + return None + + async def async_set_climate(self, toggle): + """Set climate control mode via Nissan servers.""" + climate_result = None + if toggle: + _LOGGER.info("Requesting climate turn on for %s", self.leaf.vin) + request = await self.hass.async_add_job( + self.leaf.start_climate_control + ) + for attempt in range(MAX_RESPONSE_ATTEMPTS): + if attempt > 0: + _LOGGER.info("Climate data not in yet (%s) (%s). " + "Waiting (%s) seconds.", self.leaf.vin, + attempt, PYCARWINGS2_SLEEP) + await asyncio.sleep(PYCARWINGS2_SLEEP) + + climate_result = await self.hass.async_add_job( + self.leaf.get_start_climate_control_result, request + ) + + if climate_result is not None: + break + + else: + _LOGGER.info("Requesting climate turn off for %s", self.leaf.vin) + request = await self.hass.async_add_job( + self.leaf.stop_climate_control + ) + + for attempt in range(MAX_RESPONSE_ATTEMPTS): + if attempt > 0: + _LOGGER.debug("Climate data not in yet. (%s) (%s). " + "Waiting %s seconds", self.leaf.vin, + attempt, PYCARWINGS2_SLEEP) + await asyncio.sleep(PYCARWINGS2_SLEEP) + + climate_result = await self.hass.async_add_job( + self.leaf.get_stop_climate_control_result, request + ) + + if climate_result is not None: + break + + if climate_result is not None: + _LOGGER.debug("Climate result: %s", climate_result.__dict__) + async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) + return climate_result.is_hvac_running == toggle + + _LOGGER.debug("Climate result not returned by Nissan servers") + return False + + async def async_get_location(self): + """Get location from Nissan servers.""" + request = await self.hass.async_add_job(self.leaf.request_location) + for attempt in range(MAX_RESPONSE_ATTEMPTS): + if attempt > 0: + _LOGGER.debug("Location data not in yet. (%s) (%s). " + "Waiting %s seconds", self.leaf.vin, + attempt, PYCARWINGS2_SLEEP) + await asyncio.sleep(PYCARWINGS2_SLEEP) + + location_status = await self.hass.async_add_job( + self.leaf.get_status_from_location, request + ) + + if location_status is not None: + _LOGGER.debug("Location_status=%s", location_status.__dict__) + break + + return location_status + + async def async_start_charging(self): + """Request start charging via Nissan servers.""" + # Send the command to request charging is started to Nissan servers. + # If that completes OK then trigger a fresh update to pull the + # charging status from the car after waiting a minute for the + # charging request to reach the car. + result = await self.hass.async_add_job(self.leaf.start_charging) + if result: + _LOGGER.debug("Start charging sent, " + "request updated data in 1 minute") + check_charge_at = utcnow() + timedelta(minutes=1) + self.next_update = check_charge_at + async_track_point_in_utc_time( + self.hass, self.async_update_data, check_charge_at) + + +class LeafEntity(Entity): + """Base class for Nissan Leaf entity.""" + + def __init__(self, car): + """Store LeafDataStore upon init.""" + self.car = car + + def log_registration(self): + """Log registration.""" + _LOGGER.debug( + "Registered %s component for VIN %s", + self.__class__.__name__, self.car.leaf.vin) + + @property + def device_state_attributes(self): + """Return default attributes for Nissan leaf entities.""" + return { + 'next_update': self.car.next_update, + 'last_attempt': self.car.last_check, + 'updated_on': self.car.last_battery_response, + 'update_in_progress': self.car.request_in_progress, + 'location_updated_on': self.car.last_location_response, + 'vin': self.car.leaf.vin, + } + + async def async_added_to_hass(self): + """Register callbacks.""" + self.log_registration() + async_dispatcher_connect( + self.car.hass, SIGNAL_UPDATE_LEAF, self._update_callback) + + def _update_callback(self): + """Update the state.""" + self.schedule_update_ha_state(True) diff --git a/homeassistant/components/nissan_leaf/binary_sensor.py b/homeassistant/components/nissan_leaf/binary_sensor.py new file mode 100644 index 00000000000..05255d616c4 --- /dev/null +++ b/homeassistant/components/nissan_leaf/binary_sensor.py @@ -0,0 +1,44 @@ +"""Plugged In Status Support for the Nissan Leaf.""" +import logging + +from homeassistant.components.nissan_leaf import ( + DATA_LEAF, DATA_PLUGGED_IN, LeafEntity) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['nissan_leaf'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up of a Nissan Leaf binary sensor.""" + _LOGGER.debug( + "binary_sensor setup_platform, discovery_info=%s", discovery_info) + + devices = [] + for key, value in hass.data[DATA_LEAF].items(): + _LOGGER.debug( + "binary_sensor setup_platform, key=%s, value=%s", key, value) + devices.append(LeafPluggedInSensor(value)) + + add_devices(devices, True) + + +class LeafPluggedInSensor(LeafEntity): + """Plugged In Sensor class.""" + + @property + def name(self): + """Sensor name.""" + return "{} {}".format(self.car.leaf.nickname, "Plug Status") + + @property + def state(self): + """Return true if plugged in.""" + return self.car.data[DATA_PLUGGED_IN] + + @property + def icon(self): + """Icon handling.""" + if self.car.data[DATA_PLUGGED_IN]: + return 'mdi:power-plug' + return 'mdi:power-plug-off' diff --git a/homeassistant/components/nissan_leaf/device_tracker.py b/homeassistant/components/nissan_leaf/device_tracker.py new file mode 100644 index 00000000000..163675319ea --- /dev/null +++ b/homeassistant/components/nissan_leaf/device_tracker.py @@ -0,0 +1,46 @@ +"""Support for tracking a Nissan Leaf.""" +import logging + +from homeassistant.components.nissan_leaf import ( + DATA_LEAF, DATA_LOCATION, SIGNAL_UPDATE_LEAF) +from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['nissan_leaf'] + +ICON_CAR = "mdi:car" + + +def setup_scanner(hass, config, see, discovery_info=None): + """Set up the Nissan Leaf tracker.""" + _LOGGER.debug("Setting up Scanner (device_tracker) for Nissan Leaf, " + "discovery_info=%s", discovery_info) + + def see_vehicle(): + """Handle the reporting of the vehicle position.""" + for key, value in hass.data[DATA_LEAF].items(): + host_name = value.leaf.nickname + dev_id = 'nissan_leaf_{}'.format(slugify(host_name)) + if not value.data[DATA_LOCATION]: + _LOGGER.debug("No position found for vehicle %s", key) + return False + _LOGGER.debug("Updating device_tracker for %s with position %s", + value.leaf.nickname, + value.data[DATA_LOCATION].__dict__) + attrs = { + 'updated_on': value.last_location_response, + } + see(dev_id=dev_id, + host_name=host_name, + gps=( + value.data[DATA_LOCATION].latitude, + value.data[DATA_LOCATION].longitude + ), + attributes=attrs, + icon=ICON_CAR) + + dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle) + + return True diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py new file mode 100644 index 00000000000..3c8f9ab9ef3 --- /dev/null +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -0,0 +1,113 @@ +"""Battery Charge and Range Support for the Nissan Leaf.""" +import logging + +from homeassistant.components.nissan_leaf import ( + DATA_BATTERY, DATA_CHARGING, DATA_LEAF, DATA_RANGE_AC, DATA_RANGE_AC_OFF, + LeafEntity) +from homeassistant.const import DEVICE_CLASS_BATTERY +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.util.distance import LENGTH_KILOMETERS, LENGTH_MILES +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['nissan_leaf'] + +ICON_RANGE = 'mdi:speedometer' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Sensors setup.""" + _LOGGER.debug("setup_platform nissan_leaf sensors, discovery_info=%s", + discovery_info) + + devices = [] + for key, value in hass.data[DATA_LEAF].items(): + _LOGGER.debug("adding sensor for item key=%s, value=%s", key, value) + devices.append(LeafBatterySensor(value)) + devices.append(LeafRangeSensor(value, True)) + devices.append(LeafRangeSensor(value, False)) + + add_devices(devices, True) + + +class LeafBatterySensor(LeafEntity): + """Nissan Leaf Battery Sensor.""" + + @property + def name(self): + """Sensor Name.""" + return self.car.leaf.nickname + " Charge" + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def state(self): + """Battery state percentage.""" + return round(self.car.data[DATA_BATTERY]) + + @property + def unit_of_measurement(self): + """Battery state measured in percentage.""" + return '%' + + @property + def icon(self): + """Battery state icon handling.""" + chargestate = self.car.data[DATA_CHARGING] + return icon_for_battery_level( + battery_level=self.state, + charging=chargestate + ) + + +class LeafRangeSensor(LeafEntity): + """Nissan Leaf Range Sensor.""" + + def __init__(self, car, ac_on): + """Set-up range sensor. Store if AC on.""" + self._ac_on = ac_on + super().__init__(car) + + @property + def name(self): + """Update sensor name depending on AC.""" + if self._ac_on is True: + return self.car.leaf.nickname + " Range (AC)" + return self.car.leaf.nickname + " Range" + + def log_registration(self): + """Log registration.""" + _LOGGER.debug( + "Registered LeafRangeSensor component with HASS for VIN %s", + self.car.leaf.vin) + + @property + def state(self): + """Battery range in miles or kms.""" + if self._ac_on: + ret = self.car.data[DATA_RANGE_AC] + else: + ret = self.car.data[DATA_RANGE_AC_OFF] + + if (not self.car.hass.config.units.is_metric or + self.car.force_miles): + ret = IMPERIAL_SYSTEM.length(ret, METRIC_SYSTEM.length_unit) + + return round(ret) + + @property + def unit_of_measurement(self): + """Battery range unit.""" + if (not self.car.hass.config.units.is_metric or + self.car.force_miles): + return LENGTH_MILES + return LENGTH_KILOMETERS + + @property + def icon(self): + """Nice icon for range.""" + return ICON_RANGE diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py new file mode 100644 index 00000000000..914e85b48a6 --- /dev/null +++ b/homeassistant/components/nissan_leaf/switch.py @@ -0,0 +1,99 @@ +"""Charge and Climate Control Support for the Nissan Leaf.""" +import logging + +from homeassistant.components.nissan_leaf import ( + DATA_CHARGING, DATA_CLIMATE, DATA_LEAF, LeafEntity) +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['nissan_leaf'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Nissan Leaf switch platform setup.""" + _LOGGER.debug( + "In switch setup platform, discovery_info=%s", discovery_info) + + devices = [] + for value in hass.data[DATA_LEAF].values(): + devices.append(LeafChargeSwitch(value)) + devices.append(LeafClimateSwitch(value)) + + add_devices(devices, True) + + +class LeafClimateSwitch(LeafEntity, ToggleEntity): + """Nissan Leaf Climate Control switch.""" + + @property + def name(self): + """Switch name.""" + return "{} {}".format(self.car.leaf.nickname, "Climate Control") + + def log_registration(self): + """Log registration.""" + _LOGGER.debug( + "Registered LeafClimateSwitch component with HASS for VIN %s", + self.car.leaf.vin) + + @property + def device_state_attributes(self): + """Return climate control attributes.""" + attrs = super(LeafClimateSwitch, self).device_state_attributes + attrs["updated_on"] = self.car.last_climate_response + return attrs + + @property + def is_on(self): + """Return true if climate control is on.""" + return self.car.data[DATA_CLIMATE] + + async def async_turn_on(self, **kwargs): + """Turn on climate control.""" + if await self.car.async_set_climate(True): + self.car.data[DATA_CLIMATE] = True + + async def async_turn_off(self, **kwargs): + """Turn off climate control.""" + if await self.car.async_set_climate(False): + self.car.data[DATA_CLIMATE] = False + + @property + def icon(self): + """Climate control icon.""" + if self.car.data[DATA_CLIMATE]: + return 'mdi:fan' + return 'mdi:fan-off' + + +class LeafChargeSwitch(LeafEntity, ToggleEntity): + """Nissan Leaf Charging On switch.""" + + @property + def name(self): + """Switch name.""" + return "{} {}".format(self.car.leaf.nickname, "Charging Status") + + @property + def icon(self): + """Charging switch icon.""" + if self.car.data[DATA_CHARGING]: + return 'mdi:flash' + return 'mdi:flash-off' + + @property + def is_on(self): + """Return true if charging.""" + return self.car.data[DATA_CHARGING] + + async def async_turn_on(self, **kwargs): + """Start car charging.""" + if await self.car.async_start_charging(): + self.car.data[DATA_CHARGING] = True + + def turn_off(self, **kwargs): + """Nissan API doesn't allow stopping of charge remotely.""" + _LOGGER.info( + "Cannot turn off Leaf charging." + " Nissan API does not support stopping charge remotely") diff --git a/requirements_all.txt b/requirements_all.txt index 9061348a41c..eba370c2e4e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -949,6 +949,9 @@ pyblackbird==0.5 # homeassistant.components.neato pybotvac==0.0.13 +# homeassistant.components.nissan_leaf +pycarwings2==2.8 + # homeassistant.components.cloudflare pycfdns==0.0.1 From 7d0f847f83ed248732882584afb267aac7704756 Mon Sep 17 00:00:00 2001 From: Jonas Pedersen Date: Fri, 15 Feb 2019 14:54:25 +0100 Subject: [PATCH 018/253] Add switch platform for Danfoss Air and additional sensors. (#21046) * Add switch platform for Danfoss Air and additional sensors. * Solve lint issues. * Correct style. * Minor changes * Minor changes * Minor changes * Update file header * Remove space * Remove space --- .../components/danfoss_air/__init__.py | 20 +++++- .../components/danfoss_air/binary_sensor.py | 20 +++--- .../components/danfoss_air/sensor.py | 46 ++++++++---- .../components/danfoss_air/switch.py | 72 +++++++++++++++++++ requirements_all.txt | 2 +- 5 files changed, 133 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/danfoss_air/switch.py diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py index d6123a25f23..f4a7b92c17c 100644 --- a/homeassistant/components/danfoss_air/__init__.py +++ b/homeassistant/components/danfoss_air/__init__.py @@ -9,11 +9,11 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['pydanfossair==0.0.6'] +REQUIREMENTS = ['pydanfossair==0.0.7'] _LOGGER = logging.getLogger(__name__) -DANFOSS_AIR_PLATFORMS = ['sensor', 'binary_sensor'] +DANFOSS_AIR_PLATFORMS = ['sensor', 'binary_sensor', 'switch'] DOMAIN = 'danfoss_air' MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) @@ -52,6 +52,10 @@ class DanfossAir: """Get value for sensor.""" return self._data.get(item) + def update_state(self, command, state_command): + """Send update command to Danfoss Air CCM.""" + self._data[state_command] = self._client.command(command) + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Use the data from Danfoss Air API.""" @@ -71,5 +75,17 @@ class DanfossAir: = round(self._client.command(ReadCommand.filterPercent), 2) self._data[ReadCommand.bypass] \ = self._client.command(ReadCommand.bypass) + self._data[ReadCommand.fan_step] \ + = self._client.command(ReadCommand.fan_step) + self._data[ReadCommand.supply_fan_speed] \ + = self._client.command(ReadCommand.supply_fan_speed) + self._data[ReadCommand.exhaust_fan_speed] \ + = self._client.command(ReadCommand.exhaust_fan_speed) + self._data[ReadCommand.away_mode] \ + = self._client.command(ReadCommand.away_mode) + self._data[ReadCommand.boost] \ + = self._client.command(ReadCommand.boost) + self._data[ReadCommand.battery_percent] \ + = self._client.command(ReadCommand.battery_percent) _LOGGER.debug("Done fetching data from Danfoss Air CCM module") diff --git a/homeassistant/components/danfoss_air/binary_sensor.py b/homeassistant/components/danfoss_air/binary_sensor.py index bf8fe952993..4052a100540 100644 --- a/homeassistant/components/danfoss_air/binary_sensor.py +++ b/homeassistant/components/danfoss_air/binary_sensor.py @@ -1,9 +1,4 @@ -""" -Support for the for Danfoss Air HRV binary sensor platform. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.danfoss_air/ -""" +"""Support for the for Danfoss Air HRV binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.danfoss_air import DOMAIN \ as DANFOSS_AIR_DOMAIN @@ -14,12 +9,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): from pydanfossair.commands import ReadCommand data = hass.data[DANFOSS_AIR_DOMAIN] - sensors = [["Danfoss Air Bypass Active", ReadCommand.bypass]] + sensors = [ + ["Danfoss Air Bypass Active", ReadCommand.bypass, "opening"], + ["Danfoss Air Away Mode Active", ReadCommand.away_mode, None], + ] dev = [] for sensor in sensors: - dev.append(DanfossAirBinarySensor(data, sensor[0], sensor[1])) + dev.append(DanfossAirBinarySensor( + data, sensor[0], sensor[1], sensor[2])) add_entities(dev, True) @@ -27,12 +26,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DanfossAirBinarySensor(BinarySensorDevice): """Representation of a Danfoss Air binary sensor.""" - def __init__(self, data, name, sensor_type): + def __init__(self, data, name, sensor_type, device_class): """Initialize the Danfoss Air binary sensor.""" self._data = data self._name = name self._state = None self._type = sensor_type + self._device_class = device_class @property def name(self): @@ -47,7 +47,7 @@ class DanfossAirBinarySensor(BinarySensorDevice): @property def device_class(self): """Type of device class.""" - return "opening" + return self._device_class def update(self): """Fetch new state data for the sensor.""" diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index 2f3807c4999..9902184e624 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -1,14 +1,15 @@ -""" -Support for the for Danfoss Air HRV sensor platform. +"""Support for the for Danfoss Air HRV sensors.""" +import logging -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sensor.danfoss_air/ -""" from homeassistant.components.danfoss_air import DOMAIN \ as DANFOSS_AIR_DOMAIN -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + TEMP_CELSIUS, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY) from homeassistant.helpers.entity import Entity +_LOGGER = logging.getLogger(__name__) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available Danfoss Air sensors etc.""" @@ -18,23 +19,32 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [ ["Danfoss Air Exhaust Temperature", TEMP_CELSIUS, - ReadCommand.exhaustTemperature], + ReadCommand.exhaustTemperature, DEVICE_CLASS_TEMPERATURE], ["Danfoss Air Outdoor Temperature", TEMP_CELSIUS, - ReadCommand.outdoorTemperature], + ReadCommand.outdoorTemperature, DEVICE_CLASS_TEMPERATURE], ["Danfoss Air Supply Temperature", TEMP_CELSIUS, - ReadCommand.supplyTemperature], + ReadCommand.supplyTemperature, DEVICE_CLASS_TEMPERATURE], ["Danfoss Air Extract Temperature", TEMP_CELSIUS, - ReadCommand.extractTemperature], + ReadCommand.extractTemperature, DEVICE_CLASS_TEMPERATURE], ["Danfoss Air Remaining Filter", '%', - ReadCommand.filterPercent], + ReadCommand.filterPercent, None], ["Danfoss Air Humidity", '%', - ReadCommand.humidity] + ReadCommand.humidity, DEVICE_CLASS_HUMIDITY], + ["Danfoss Air Fan Step", '%', + ReadCommand.fan_step, None], + ["Dandoss Air Exhaust Fan Speed", 'RPM', + ReadCommand.exhaust_fan_speed, None], + ["Dandoss Air Supply Fan Speed", 'RPM', + ReadCommand.supply_fan_speed, None], + ["Dandoss Air Dial Battery", '%', + ReadCommand.battery_percent, DEVICE_CLASS_BATTERY] ] dev = [] for sensor in sensors: - dev.append(DanfossAir(data, sensor[0], sensor[1], sensor[2])) + dev.append(DanfossAir( + data, sensor[0], sensor[1], sensor[2], sensor[3])) add_entities(dev, True) @@ -42,19 +52,25 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class DanfossAir(Entity): """Representation of a Sensor.""" - def __init__(self, data, name, sensor_unit, sensor_type): + def __init__(self, data, name, sensor_unit, sensor_type, device_class): """Initialize the sensor.""" self._data = data self._name = name self._state = None self._type = sensor_type self._unit = sensor_unit + self._device_class = device_class @property def name(self): """Return the name of the sensor.""" return self._name + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class + @property def state(self): """Return the state of the sensor.""" @@ -74,3 +90,5 @@ class DanfossAir(Entity): self._data.update() self._state = self._data.get_value(self._type) + if self._state is None: + _LOGGER.debug("Could not get data for %s", self._type) diff --git a/homeassistant/components/danfoss_air/switch.py b/homeassistant/components/danfoss_air/switch.py new file mode 100644 index 00000000000..ec85757be59 --- /dev/null +++ b/homeassistant/components/danfoss_air/switch.py @@ -0,0 +1,72 @@ +"""Support for the for Danfoss Air HRV sswitches.""" +import logging + +from homeassistant.components.switch import ( + SwitchDevice) +from homeassistant.components.danfoss_air import DOMAIN \ + as DANFOSS_AIR_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Danfoss Air HRV switch platform.""" + from pydanfossair.commands import ReadCommand, UpdateCommand + + data = hass.data[DANFOSS_AIR_DOMAIN] + + switches = [ + ["Danfoss Air Boost", + ReadCommand.boost, + UpdateCommand.boost_activate, + UpdateCommand.boost_deactivate], + ] + + dev = [] + + for switch in switches: + dev.append(DanfossAir( + data, switch[0], switch[1], switch[2], switch[3])) + + add_entities(dev) + + +class DanfossAir(SwitchDevice): + """Representation of a Danfoss Air HRV Switch.""" + + def __init__(self, data, name, state_command, on_command, off_command): + """Initialize the switch.""" + self._data = data + self._name = name + self._state_command = state_command + self._on_command = on_command + self._off_command = off_command + self._state = None + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def is_on(self): + """Return true if switch is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the switch on.""" + _LOGGER.debug("Turning on switch with command %s", self._on_command) + self._data.update_state(self._on_command, self._state_command) + + def turn_off(self, **kwargs): + """Turn the switch off.""" + _LOGGER.debug("Turning of switch with command %s", self._off_command) + self._data.update_state(self._off_command, self._state_command) + + def update(self): + """Update the switch's state.""" + self._data.update() + + self._state = self._data.get_value(self._state_command) + if self._state is None: + _LOGGER.debug("Could not get data for %s", self._state_command) diff --git a/requirements_all.txt b/requirements_all.txt index eba370c2e4e..222c966aa29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -980,7 +980,7 @@ pycsspeechtts==1.0.2 pydaikin==0.9 # homeassistant.components.danfoss_air -pydanfossair==0.0.6 +pydanfossair==0.0.7 # homeassistant.components.deconz pydeconz==47 From 93f84a5cd186bb75c283530c114f9bb51b8a3c0b Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 15 Feb 2019 10:40:54 -0600 Subject: [PATCH 019/253] SmartThings Component Enhancements/Fixes (#21085) * Improve component setup error logging/notification * Prevent capabilities from being represented my multiple platforms * Improved logging of received updates * Updates based on review feedback --- .../smartthings/.translations/en.json | 3 +- .../components/smartthings/__init__.py | 47 +++++++++++++++++-- .../components/smartthings/binary_sensor.py | 15 ++++-- .../components/smartthings/climate.py | 36 ++++++++------ .../components/smartthings/config_flow.py | 13 ++++- homeassistant/components/smartthings/const.py | 8 ++-- homeassistant/components/smartthings/fan.py | 14 ++++-- homeassistant/components/smartthings/light.py | 26 +++++----- homeassistant/components/smartthings/lock.py | 13 +++-- .../components/smartthings/sensor.py | 21 ++++++--- .../components/smartthings/strings.json | 3 +- .../components/smartthings/switch.py | 25 ++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/smartthings/test_climate.py | 13 ----- .../smartthings/test_config_flow.py | 44 +++++++++++++++-- tests/components/smartthings/test_fan.py | 20 -------- tests/components/smartthings/test_light.py | 19 -------- tests/components/smartthings/test_lock.py | 6 --- tests/components/smartthings/test_switch.py | 17 ------- 20 files changed, 196 insertions(+), 151 deletions(-) diff --git a/homeassistant/components/smartthings/.translations/en.json b/homeassistant/components/smartthings/.translations/en.json index f2775b30ae2..2091ddb00a2 100644 --- a/homeassistant/components/smartthings/.translations/en.json +++ b/homeassistant/components/smartthings/.translations/en.json @@ -7,7 +7,8 @@ "token_already_setup": "The token has already been setup.", "token_forbidden": "The token does not have the required OAuth scopes.", "token_invalid_format": "The token must be in the UID/GUID format", - "token_unauthorized": "The token is invalid or no longer authorized." + "token_unauthorized": "The token is invalid or no longer authorized.", + "webhook_error": "SmartThings could not validate the endpoint configured in `base_url`. Please review the [component requirements]({component_url})." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 04da29aa55e..3cf38c358bc 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -1,5 +1,6 @@ """Support for SmartThings Cloud.""" import asyncio +import importlib import logging from typing import Iterable @@ -22,7 +23,7 @@ from .const import ( from .smartapp import ( setup_smartapp, setup_smartapp_endpoint, validate_installed_app) -REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.1'] +REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.2'] DEPENDENCIES = ['webhook'] _LOGGER = logging.getLogger(__name__) @@ -132,9 +133,41 @@ class DeviceBroker: """Create a new instance of the DeviceBroker.""" self._hass = hass self._installed_app_id = installed_app_id + self.assignments = self._assign_capabilities(devices) self.devices = {device.device_id: device for device in devices} self.event_handler_disconnect = None + def _assign_capabilities(self, devices: Iterable): + """Assign platforms to capabilities.""" + assignments = {} + for device in devices: + capabilities = device.capabilities.copy() + slots = {} + for platform_name in SUPPORTED_PLATFORMS: + platform = importlib.import_module( + '.' + platform_name, self.__module__) + assigned = platform.get_capabilities(capabilities) + if not assigned: + continue + # Draw-down capabilities and set slot assignment + for capability in assigned: + if capability not in capabilities: + continue + capabilities.remove(capability) + slots[capability] = platform_name + assignments[device.device_id] = slots + return assignments + + def get_assigned(self, device_id: str, platform: str): + """Get the capabilities assigned to the platform.""" + slots = self.assignments.get(device_id, {}) + return [key for key, value in slots.items() if value == platform] + + def any_assigned(self, device_id: str, platform: str): + """Return True if the platform has any assigned capabilities.""" + slots = self.assignments.get(device_id, {}) + return any(value for value in slots.values() if value == platform) + async def event_handler(self, req, resp, app): """Broker for incoming events.""" from pysmartapp.event import EVENT_TYPE_DEVICE @@ -167,10 +200,18 @@ class DeviceBroker: } self._hass.bus.async_fire(EVENT_BUTTON, data) _LOGGER.debug("Fired button event: %s", data) + else: + data = { + 'location_id': evt.location_id, + 'device_id': evt.device_id, + 'component_id': evt.component_id, + 'capability': evt.capability, + 'attribute': evt.attribute, + 'value': evt.value, + } + _LOGGER.debug("Push update received: %s", data) updated_devices.add(device.device_id) - _LOGGER.debug("Update received with %s events and updated %s devices", - len(req.events), len(updated_devices)) async_dispatcher_send(self._hass, SIGNAL_SMARTTHINGS_UPDATE, updated_devices) diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 2fbb6f719da..45101601d5f 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -1,4 +1,6 @@ """Support for binary sensors through the SmartThings cloud API.""" +from typing import Optional, Sequence + from homeassistant.components.binary_sensor import BinarySensorDevice from . import SmartThingsEntity @@ -41,12 +43,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] sensors = [] for device in broker.devices.values(): - for capability, attrib in CAPABILITY_TO_ATTRIB.items(): - if capability in device.capabilities: - sensors.append(SmartThingsBinarySensor(device, attrib)) + for capability in broker.get_assigned( + device.device_id, 'binary_sensor'): + attrib = CAPABILITY_TO_ATTRIB[capability] + sensors.append(SmartThingsBinarySensor(device, attrib)) async_add_entities(sensors) +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" + return [capability for capability in CAPABILITY_TO_ATTRIB + if capability in capabilities] + + class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorDevice): """Define a SmartThings Binary Sensor.""" diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index d70d865202d..d1f58cf91f1 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -1,11 +1,12 @@ """Support for climate devices through the SmartThings cloud API.""" import asyncio +from typing import Optional, Sequence from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, - SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_FAN_MODE, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -49,30 +50,37 @@ async def async_setup_entry(hass, config_entry, async_add_entities): broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] async_add_entities( [SmartThingsThermostat(device) for device in broker.devices.values() - if is_climate(device)]) + if broker.any_assigned(device.device_id, 'climate')]) -def is_climate(device): - """Determine if the device should be represented as a climate entity.""" +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" from pysmartthings import Capability + supported = [ + Capability.thermostat, + Capability.temperature_measurement, + Capability.thermostat_cooling_setpoint, + Capability.thermostat_heating_setpoint, + Capability.thermostat_mode, + Capability.relative_humidity_measurement, + Capability.thermostat_operating_state, + Capability.thermostat_fan_mode + ] # Can have this legacy/deprecated capability - if Capability.thermostat in device.capabilities: - return True + if Capability.thermostat in capabilities: + return supported # Or must have all of these climate_capabilities = [ Capability.temperature_measurement, Capability.thermostat_cooling_setpoint, Capability.thermostat_heating_setpoint, Capability.thermostat_mode] - if all(capability in device.capabilities + if all(capability in capabilities for capability in climate_capabilities): - return True - # Optional capabilities: - # relative_humidity_measurement -> state attribs - # thermostat_operating_state -> state attribs - # thermostat_fan_mode -> SUPPORT_FAN_MODE - return False + return supported + + return None class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index b280036a615..4663222c3b4 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure SmartThings.""" import logging -from aiohttp.client_exceptions import ClientResponseError +from aiohttp import ClientResponseError import voluptuous as vol from homeassistant import config_entries @@ -50,7 +50,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Get access token and validate it.""" - from pysmartthings import SmartThings + from pysmartthings import APIResponseError, SmartThings errors = {} if not self.hass.config.api.base_url.lower().startswith('https://'): @@ -87,6 +87,14 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): app = await create_app(self.hass, self.api) setup_smartapp(self.hass, app) self.app_id = app.app_id + except APIResponseError as ex: + if ex.is_target_error(): + errors['base'] = 'webhook_error' + else: + errors['base'] = "app_setup_error" + _LOGGER.exception("API error setting up the SmartApp: %s", + ex.raw_error_response) + return self._show_step_user(errors) except ClientResponseError as ex: if ex.status == 401: errors[CONF_ACCESS_TOKEN] = "token_unauthorized" @@ -94,6 +102,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): errors[CONF_ACCESS_TOKEN] = "token_forbidden" else: errors['base'] = "app_setup_error" + _LOGGER.exception("Unexpected error setting up the SmartApp") return self._show_step_user(errors) except Exception: # pylint:disable=broad-except errors['base'] = "app_setup_error" diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index 25cd9e8305f..27260b155d1 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -18,14 +18,16 @@ SIGNAL_SMARTAPP_PREFIX = 'smartthings_smartap_' SETTINGS_INSTANCE_ID = "hassInstanceId" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 +# Ordered 'specific to least-specific platform' in order for capabilities +# to be drawn-down and represented by the appropriate platform. SUPPORTED_PLATFORMS = [ - 'binary_sensor', 'climate', 'fan', 'light', 'lock', - 'sensor', - 'switch' + 'switch', + 'binary_sensor', + 'sensor' ] VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]" \ "{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$" diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index 4de1744c9b8..e722cd21d65 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -1,4 +1,6 @@ """Support for fans through the SmartThings cloud API.""" +from typing import Optional, Sequence + from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, FanEntity) @@ -29,15 +31,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] async_add_entities( [SmartThingsFan(device) for device in broker.devices.values() - if is_fan(device)]) + if broker.any_assigned(device.device_id, 'fan')]) -def is_fan(device): - """Determine if the device should be represented as a fan.""" +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" from pysmartthings import Capability + + supported = [Capability.switch, Capability.fan_speed] # Must have switch and fan_speed - return all(capability in device.capabilities - for capability in [Capability.switch, Capability.fan_speed]) + if all(capability in capabilities for capability in supported): + return supported class SmartThingsFan(SmartThingsEntity, FanEntity): diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py index ce4b00ca1fe..79a5eabc20a 100644 --- a/homeassistant/components/smartthings/light.py +++ b/homeassistant/components/smartthings/light.py @@ -1,5 +1,6 @@ """Support for lights through the SmartThings cloud API.""" import asyncio +from typing import Optional, Sequence from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, @@ -24,29 +25,32 @@ async def async_setup_entry(hass, config_entry, async_add_entities): broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] async_add_entities( [SmartThingsLight(device) for device in broker.devices.values() - if is_light(device)], True) + if broker.any_assigned(device.device_id, 'light')], True) -def is_light(device): - """Determine if the device should be represented as a light.""" +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" from pysmartthings import Capability + supported = [ + Capability.switch, + Capability.switch_level, + Capability.color_control, + Capability.color_temperature, + ] # Must be able to be turned on/off. - if Capability.switch not in device.capabilities: - return False - # Not a fan (which might also have switch_level) - if Capability.fan_speed in device.capabilities: - return False + if Capability.switch not in capabilities: + return None # Must have one of these light_capabilities = [ Capability.color_control, Capability.color_temperature, Capability.switch_level ] - if any(capability in device.capabilities + if any(capability in capabilities for capability in light_capabilities): - return True - return False + return supported + return None def convert_scale(value, value_scale, target_scale, round_digits=4): diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py index e756cbfa918..bc5ab7a8ccd 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -1,4 +1,6 @@ """Support for locks through the SmartThings cloud API.""" +from typing import Optional, Sequence + from homeassistant.components.lock import LockDevice from . import SmartThingsEntity @@ -25,13 +27,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] async_add_entities( [SmartThingsLock(device) for device in broker.devices.values() - if is_lock(device)]) + if broker.any_assigned(device.device_id, 'lock')]) -def is_lock(device): - """Determine if the device supports the lock capability.""" +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" from pysmartthings import Capability - return Capability.lock in device.capabilities + + if Capability.lock in capabilities: + return [Capability.lock] + return None class SmartThingsLock(SmartThingsEntity, LockDevice): diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index eb83334c6b3..32047c179b4 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -1,5 +1,6 @@ """Support for sensors through the SmartThings cloud API.""" from collections import namedtuple +from typing import Optional, Sequence from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -164,16 +165,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities): broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] sensors = [] for device in broker.devices.values(): - for capability, maps in CAPABILITY_TO_SENSORS.items(): - if capability in device.capabilities: - sensors.extend([ - SmartThingsSensor( - device, m.attribute, m.name, m.default_unit, - m.device_class) - for m in maps]) + for capability in broker.get_assigned(device.device_id, 'sensor'): + maps = CAPABILITY_TO_SENSORS[capability] + sensors.extend([ + SmartThingsSensor( + device, m.attribute, m.name, m.default_unit, + m.device_class) + for m in maps]) async_add_entities(sensors) +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" + return [capability for capability in CAPABILITY_TO_SENSORS + if capability in capabilities] + + class SmartThingsSensor(SmartThingsEntity): """Define a SmartThings Binary Sensor.""" diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 1fb4e878cb4..bcbe02f6011 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -21,7 +21,8 @@ "token_already_setup": "The token has already been setup.", "app_setup_error": "Unable to setup the SmartApp. Please try again.", "app_not_installed": "Please ensure you have installed and authorized the Home Assistant SmartApp and try again.", - "base_url_not_https": "The `base_url` for the `http` component must be configured and start with `https://`." + "base_url_not_https": "The `base_url` for the `http` component must be configured and start with `https://`.", + "webhook_error": "SmartThings could not validate the endpoint configured in `base_url`. Please review the [component requirements]({component_url})." } } } \ No newline at end of file diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index 08cdb74ed77..5a1224f4fc2 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -1,4 +1,6 @@ """Support for switches through the SmartThings cloud API.""" +from typing import Optional, Sequence + from homeassistant.components.switch import SwitchDevice from . import SmartThingsEntity @@ -18,28 +20,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] async_add_entities( [SmartThingsSwitch(device) for device in broker.devices.values() - if is_switch(device)]) + if broker.any_assigned(device.device_id, 'switch')]) -def is_switch(device): - """Determine if the device should be represented as a switch.""" +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" from pysmartthings import Capability # Must be able to be turned on/off. - if Capability.switch not in device.capabilities: - return False - # Must not have a capability represented by other types. - non_switch_capabilities = [ - Capability.color_control, - Capability.color_temperature, - Capability.fan_speed, - Capability.switch_level - ] - if any(capability in device.capabilities - for capability in non_switch_capabilities): - return False - - return True + if Capability.switch in capabilities: + return [Capability.switch] + return None class SmartThingsSwitch(SmartThingsEntity, SwitchDevice): diff --git a/requirements_all.txt b/requirements_all.txt index 222c966aa29..edd46dfd9a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1244,7 +1244,7 @@ pysma==0.3.1 pysmartapp==0.3.0 # homeassistant.components.smartthings -pysmartthings==0.6.1 +pysmartthings==0.6.2 # homeassistant.components.device_tracker.snmp # homeassistant.components.sensor.snmp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4ebc2301e1..50e83ba952b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -217,7 +217,7 @@ pyqwikswitch==0.8 pysmartapp==0.3.0 # homeassistant.components.smartthings -pysmartthings==0.6.1 +pysmartthings==0.6.2 # homeassistant.components.sonos pysonos==0.0.6 diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index c615ca66e34..d8b1346d225 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -100,19 +100,6 @@ async def test_async_setup_platform(): await climate.async_setup_platform(None, None, None) -def test_is_climate(device_factory, legacy_thermostat, - basic_thermostat, thermostat): - """Test climate devices are correctly identified.""" - other_devices = [ - device_factory('Unknown', ['Unknown']), - device_factory("Switch 1", [Capability.switch]) - ] - for device in [legacy_thermostat, basic_thermostat, thermostat]: - assert climate.is_climate(device), device.name - for device in other_devices: - assert not climate.is_climate(device), device.name - - async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): """Tests the state attributes properly match the thermostat type.""" await setup_platform(hass, CLIMATE_DOMAIN, legacy_thermostat) diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 4d2a43a52c7..7d335703131 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -1,8 +1,9 @@ """Tests for the SmartThings config flow module.""" -from unittest.mock import patch +from unittest.mock import Mock, patch from uuid import uuid4 -from aiohttp.client_exceptions import ClientResponseError +from aiohttp import ClientResponseError +from pysmartthings import APIResponseError from homeassistant import data_entry_flow from homeassistant.components.smartthings.config_flow import ( @@ -103,13 +104,50 @@ async def test_token_forbidden(hass, smartthings_mock): assert result['errors'] == {'access_token': 'token_forbidden'} +async def test_webhook_error(hass, smartthings_mock): + """Test an error is when there's an error with the webhook endpoint.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + + data = {'error': {}} + error = APIResponseError(None, None, data=data, status=422) + error.is_target_error = Mock(return_value=True) + + smartthings_mock.return_value.apps.return_value = mock_coro( + exception=error) + + result = await flow.async_step_user({'access_token': str(uuid4())}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + assert result['errors'] == {'base': 'webhook_error'} + + +async def test_api_error(hass, smartthings_mock): + """Test an error is shown when other API errors occur.""" + flow = SmartThingsFlowHandler() + flow.hass = hass + + data = {'error': {}} + error = APIResponseError(None, None, data=data, status=400) + + smartthings_mock.return_value.apps.return_value = mock_coro( + exception=error) + + result = await flow.async_step_user({'access_token': str(uuid4())}) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + assert result['errors'] == {'base': 'app_setup_error'} + + async def test_unknown_api_error(hass, smartthings_mock): """Test an error is shown when there is an unknown API error.""" flow = SmartThingsFlowHandler() flow.hass = hass smartthings_mock.return_value.apps.return_value = mock_coro( - exception=ClientResponseError(None, None, status=500)) + exception=ClientResponseError(None, None, status=404)) result = await flow.async_step_user({'access_token': str(uuid4())}) diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 99627e866d9..db8d9b512de 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -39,26 +39,6 @@ async def test_async_setup_platform(): await fan.async_setup_platform(None, None, None) -def test_is_fan(device_factory): - """Test fans are correctly identified.""" - non_fans = [ - device_factory('Unknown', ['Unknown']), - device_factory("Switch 1", [Capability.switch]), - device_factory("Non-Switchable Fan", [Capability.fan_speed]), - device_factory("Color Light", - [Capability.switch, Capability.switch_level, - Capability.color_control, - Capability.color_temperature]) - ] - fan_device = device_factory( - "Fan 1", [Capability.switch, Capability.switch_level, - Capability.fan_speed]) - - assert fan.is_fan(fan_device), fan_device.name - for device in non_fans: - assert not fan.is_fan(device), device.name - - async def test_entity_state(hass, device_factory): """Tests the state attributes properly match the fan types.""" device = device_factory( diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index a4f1103f270..72bc5da9063 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -65,25 +65,6 @@ async def test_async_setup_platform(): await light.async_setup_platform(None, None, None) -def test_is_light(device_factory, light_devices): - """Test lights are correctly identified.""" - non_lights = [ - device_factory('Unknown', ['Unknown']), - device_factory("Fan 1", - [Capability.switch, Capability.switch_level, - Capability.fan_speed]), - device_factory("Switch 1", [Capability.switch]), - device_factory("Can't be turned off", - [Capability.switch_level, Capability.color_control, - Capability.color_temperature]) - ] - - for device in light_devices: - assert light.is_light(device), device.name - for device in non_lights: - assert not light.is_light(device), device.name - - async def test_entity_state(hass, light_devices): """Tests the state attributes properly match the light types.""" await _setup_platform(hass, *light_devices) diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index c73f4ff549e..3739a2dc9b5 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -20,12 +20,6 @@ async def test_async_setup_platform(): await lock.async_setup_platform(None, None, None) -def test_is_lock(device_factory): - """Test locks are correctly identified.""" - lock_device = device_factory('Lock', [Capability.lock]) - assert lock.is_lock(lock_device) - - async def test_entity_and_device_attributes(hass, device_factory): """Test the attributes of the entity are correct.""" # Arrange diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 15ff3adce86..3f2bedd4f13 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -35,23 +35,6 @@ async def test_async_setup_platform(): await switch.async_setup_platform(None, None, None) -def test_is_switch(device_factory): - """Test switches are correctly identified.""" - switch_device = device_factory('Switch', [Capability.switch]) - non_switch_devices = [ - device_factory('Light', [Capability.switch, Capability.switch_level]), - device_factory('Fan', [Capability.switch, Capability.fan_speed]), - device_factory('Color Light', [Capability.switch, - Capability.color_control]), - device_factory('Temp Light', [Capability.switch, - Capability.color_temperature]), - device_factory('Unknown', ['Unknown']), - ] - assert switch.is_switch(switch_device) - for non_switch_device in non_switch_devices: - assert not switch.is_switch(non_switch_device) - - async def test_entity_and_device_attributes(hass, device_factory): """Test the attributes of the entity are correct.""" # Arrange From 1130ccb325a282a8b1585539699a898b45026a2f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Feb 2019 08:43:30 -0800 Subject: [PATCH 020/253] Fix hue retry crash (#21083) * Fix Hue retry crash * Fix hue retry crash * Fix tests --- homeassistant/components/hue/__init__.py | 9 ++++++--- tests/components/hue/test_init.py | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index e5f40e3ca29..104965b7b8f 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -23,6 +23,8 @@ CONF_BRIDGES = "bridges" CONF_ALLOW_UNREACHABLE = 'allow_unreachable' DEFAULT_ALLOW_UNREACHABLE = False +DATA_CONFIGS = 'hue_configs' + PHUE_CONFIG_FILE = 'phue.conf' CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" @@ -54,6 +56,7 @@ async def async_setup(hass, config): conf = {} hass.data[DOMAIN] = {} + hass.data[DATA_CONFIGS] = {} configured = configured_hosts(hass) # User has configured bridges @@ -66,7 +69,7 @@ async def async_setup(hass, config): host = bridge_conf[CONF_HOST] # Store config in hass.data so the config entry can find it - hass.data[DOMAIN][host] = bridge_conf + hass.data[DATA_CONFIGS][host] = bridge_conf # If configured, the bridge will be set up during config entry phase if host in configured: @@ -91,7 +94,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up a bridge from a config entry.""" host = entry.data['host'] - config = hass.data[DOMAIN].get(host) + config = hass.data[DATA_CONFIGS].get(host) if config is None: allow_unreachable = DEFAULT_ALLOW_UNREACHABLE @@ -101,11 +104,11 @@ async def async_setup_entry(hass, entry): allow_groups = config[CONF_ALLOW_HUE_GROUPS] bridge = HueBridge(hass, entry, allow_unreachable, allow_groups) - hass.data[DOMAIN][host] = bridge if not await bridge.async_setup(): return False + hass.data[DOMAIN][host] = bridge config = bridge.api.config device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 1fcc092dd30..6c89995a1a1 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -39,7 +39,7 @@ async def test_setup_defined_hosts_known_auth(hass): assert len(mock_config_entries.flow.mock_calls) == 0 # Config stored for domain. - assert hass.data[hue.DOMAIN] == { + assert hass.data[hue.DATA_CONFIGS] == { '0.0.0.0': { hue.CONF_HOST: '0.0.0.0', hue.CONF_FILENAME: 'bla.conf', @@ -73,7 +73,7 @@ async def test_setup_defined_hosts_no_known_auth(hass): } # Config stored for domain. - assert hass.data[hue.DOMAIN] == { + assert hass.data[hue.DATA_CONFIGS] == { '0.0.0.0': { hue.CONF_HOST: '0.0.0.0', hue.CONF_FILENAME: 'bla.conf', From 383813bfe6ceffb5dbc6aad299ea37fb1d2a710d Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 15 Feb 2019 11:30:47 -0600 Subject: [PATCH 021/253] Config Entry migrations (#20888) * Updated per review feedback. * Fixed line length * Review comments and lint error * Fixed mypy typeing error * Moved migration logic to setup * Use new migration error state * Fix bug and ignore mypy type error * Removed SmartThings example and added unit tests. * Fixed test comments. --- homeassistant/config_entries.py | 54 +++++++++++++- tests/common.py | 8 +- tests/test_config_entries.py | 126 ++++++++++++++++++++++++++++++-- 3 files changed, 180 insertions(+), 8 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c7dfc0c889b..cb79f457ce5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -7,7 +7,11 @@ component. During startup, Home Assistant will setup the entries during the normal setup of a component. It will first call the normal setup and then call the method `async_setup_entry(hass, entry)` for each entry. The same method is called when -Home Assistant is running while a config entry is created. +Home Assistant is running while a config entry is created. If the version of +the config entry does not match that of the flow handler, setup will +call the method `async_migrate_entry(hass, entry)` with the expectation that +the entry be brought to the current version. Return `True` to indicate +migration was successful, otherwise `False`. ## Config Flows @@ -116,6 +120,7 @@ If the result of the step is to show a form, the user will be able to continue the flow from the config panel. """ import logging +import functools import uuid from typing import Set, Optional, List, Dict # noqa pylint: disable=unused-import @@ -188,6 +193,8 @@ SAVE_DELAY = 1 ENTRY_STATE_LOADED = 'loaded' # There was an error while trying to set up this config entry ENTRY_STATE_SETUP_ERROR = 'setup_error' +# There was an error while trying to migrate the config entry to a new version +ENTRY_STATE_MIGRATION_ERROR = 'migration_error' # The config entry was not ready to be set up yet, but might be later ENTRY_STATE_SETUP_RETRY = 'setup_retry' # The config entry has not been loaded @@ -256,6 +263,12 @@ class ConfigEntry: if component is None: component = getattr(hass.components, self.domain) + # Perform migration + if component.DOMAIN == self.domain: + if not await self.async_migrate(hass): + self.state = ENTRY_STATE_MIGRATION_ERROR + return + try: result = await component.async_setup_entry(hass, self) @@ -332,6 +345,45 @@ class ConfigEntry: self.state = ENTRY_STATE_FAILED_UNLOAD return False + async def async_migrate(self, hass: HomeAssistant) -> bool: + """Migrate an entry. + + Returns True if config entry is up-to-date or has been migrated. + """ + handler = HANDLERS.get(self.domain) + if handler is None: + _LOGGER.error("Flow handler not found for entry %s for %s", + self.title, self.domain) + return False + # Handler may be a partial + while isinstance(handler, functools.partial): + handler = handler.func + + if self.version == handler.VERSION: + return True + + component = getattr(hass.components, self.domain) + supports_migrate = hasattr(component, 'async_migrate_entry') + if not supports_migrate: + _LOGGER.error("Migration handler not found for entry %s for %s", + self.title, self.domain) + return False + + try: + result = await component.async_migrate_entry(hass, self) + if not isinstance(result, bool): + _LOGGER.error('%s.async_migrate_entry did not return boolean', + self.domain) + return False + if result: + # pylint: disable=protected-access + hass.config_entries._async_schedule_save() # type: ignore + return result + except Exception: # pylint: disable=broad-except + _LOGGER.exception('Error migrating entry %s for %s', + self.title, component.DOMAIN) + return False + def as_dict(self): """Return dictionary version of this entry.""" return { diff --git a/tests/common.py b/tests/common.py index 409b020f728..28c6e4c5301 100644 --- a/tests/common.py +++ b/tests/common.py @@ -451,7 +451,8 @@ class MockModule: def __init__(self, domain=None, dependencies=None, setup=None, requirements=None, config_schema=None, platform_schema=None, platform_schema_base=None, async_setup=None, - async_setup_entry=None, async_unload_entry=None): + async_setup_entry=None, async_unload_entry=None, + async_migrate_entry=None): """Initialize the mock module.""" self.DOMAIN = domain self.DEPENDENCIES = dependencies or [] @@ -482,6 +483,9 @@ class MockModule: if async_unload_entry is not None: self.async_unload_entry = async_unload_entry + if async_migrate_entry is not None: + self.async_migrate_entry = async_migrate_entry + class MockPlatform: """Provide a fake platform.""" @@ -602,7 +606,7 @@ class MockToggleDevice(entity.ToggleEntity): class MockConfigEntry(config_entries.ConfigEntry): """Helper for creating config entries that adds some defaults.""" - def __init__(self, *, domain='test', data=None, version=0, entry_id=None, + def __init__(self, *, domain='test', data=None, version=1, entry_id=None, source=config_entries.SOURCE_USER, title='Mock Title', state=None, connection_class=config_entries.CONN_CLASS_UNKNOWN): diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 496ad785275..e724680a05b 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -15,6 +15,14 @@ from tests.common import ( MockPlatform, MockEntity) +@config_entries.HANDLERS.register('test') +@config_entries.HANDLERS.register('comp') +class MockFlowHandler(config_entries.ConfigFlow): + """Define a mock flow handler.""" + + VERSION = 1 + + @pytest.fixture def manager(hass): """Fixture of a loaded config manager.""" @@ -25,10 +33,117 @@ def manager(hass): return manager -@asyncio.coroutine -def test_call_setup_entry(hass): +async def test_call_setup_entry(hass): """Test we call .setup_entry.""" - MockConfigEntry(domain='comp').add_to_hass(hass) + entry = MockConfigEntry(domain='comp') + entry.add_to_hass(hass) + + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_migrate_entry = MagicMock(return_value=mock_coro(True)) + + loader.set_component( + hass, 'comp', + MockModule('comp', async_setup_entry=mock_setup_entry, + async_migrate_entry=mock_migrate_entry)) + + result = await async_setup_component(hass, 'comp', {}) + assert result + assert len(mock_migrate_entry.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 1 + assert entry.state == config_entries.ENTRY_STATE_LOADED + + +async def test_call_async_migrate_entry(hass): + """Test we call .async_migrate_entry when version mismatch.""" + entry = MockConfigEntry(domain='comp') + entry.version = 2 + entry.add_to_hass(hass) + + mock_migrate_entry = MagicMock(return_value=mock_coro(True)) + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + + loader.set_component( + hass, 'comp', + MockModule('comp', async_setup_entry=mock_setup_entry, + async_migrate_entry=mock_migrate_entry)) + + result = await async_setup_component(hass, 'comp', {}) + assert result + assert len(mock_migrate_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert entry.state == config_entries.ENTRY_STATE_LOADED + + +async def test_call_async_migrate_entry_failure_false(hass): + """Test migration fails if returns false.""" + entry = MockConfigEntry(domain='comp') + entry.version = 2 + entry.add_to_hass(hass) + + mock_migrate_entry = MagicMock(return_value=mock_coro(False)) + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + + loader.set_component( + hass, 'comp', + MockModule('comp', async_setup_entry=mock_setup_entry, + async_migrate_entry=mock_migrate_entry)) + + result = await async_setup_component(hass, 'comp', {}) + assert result + assert len(mock_migrate_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 0 + assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + + +async def test_call_async_migrate_entry_failure_exception(hass): + """Test migration fails if exception raised.""" + entry = MockConfigEntry(domain='comp') + entry.version = 2 + entry.add_to_hass(hass) + + mock_migrate_entry = MagicMock( + return_value=mock_coro(exception=Exception)) + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + + loader.set_component( + hass, 'comp', + MockModule('comp', async_setup_entry=mock_setup_entry, + async_migrate_entry=mock_migrate_entry)) + + result = await async_setup_component(hass, 'comp', {}) + assert result + assert len(mock_migrate_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 0 + assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + + +async def test_call_async_migrate_entry_failure_not_bool(hass): + """Test migration fails if boolean not returned.""" + entry = MockConfigEntry(domain='comp') + entry.version = 2 + entry.add_to_hass(hass) + + mock_migrate_entry = MagicMock( + return_value=mock_coro()) + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + + loader.set_component( + hass, 'comp', + MockModule('comp', async_setup_entry=mock_setup_entry, + async_migrate_entry=mock_migrate_entry)) + + result = await async_setup_component(hass, 'comp', {}) + assert result + assert len(mock_migrate_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 0 + assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + + +async def test_call_async_migrate_entry_failure_not_supported(hass): + """Test migration fails if async_migrate_entry not implemented.""" + entry = MockConfigEntry(domain='comp') + entry.version = 2 + entry.add_to_hass(hass) mock_setup_entry = MagicMock(return_value=mock_coro(True)) @@ -36,9 +151,10 @@ def test_call_setup_entry(hass): hass, 'comp', MockModule('comp', async_setup_entry=mock_setup_entry)) - result = yield from async_setup_component(hass, 'comp', {}) + result = await async_setup_component(hass, 'comp', {}) assert result - assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 0 + assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR async def test_remove_entry(hass, manager): From 46efc0eafbef3dde7e945c933c429dfbd19d1f81 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 15 Feb 2019 09:31:54 -0800 Subject: [PATCH 022/253] Refactor http CachingStaticResource (#21062) * Simplify http.CachingStaticResource implementation * Sync up CachingStaticResource._handle() implementation from aiohttp * Ignore pylint duplicate-base warning * Try to disable pylint for http/static.py Caused by https://github.com/PyCQA/astroid/issues/633#issuecomment-463879288 * Remove pylint ignore * Ignore pylint duplicate-base warning --- homeassistant/components/http/__init__.py | 5 +-- homeassistant/components/http/static.py | 45 +++++++++-------------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 10d6ae5148b..cdbbbe551ab 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -16,13 +16,12 @@ import homeassistant.util as hass_util from homeassistant.util import ssl as ssl_util from homeassistant.util.logging import HideSensitiveDataFilter -# Import as alias from .auth import setup_auth from .ban import setup_bans from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa from .cors import setup_cors from .real_ip import setup_real_ip -from .static import CachingFileResponse, CachingStaticResource +from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView # noqa REQUIREMENTS = ['aiohttp_cors==0.7.0'] @@ -272,7 +271,7 @@ class HomeAssistantHTTP: if cache_headers: async def serve_file(request): """Serve file from disk.""" - return CachingFileResponse(path) + return web.FileResponse(path, headers=CACHE_HEADERS) else: async def serve_file(request): """Serve file from disk.""" diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 54e72c88ff3..4fac9bf1ae9 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -1,18 +1,29 @@ """Static file handling for HTTP component.""" +from pathlib import Path + from aiohttp import hdrs from aiohttp.web import FileResponse -from aiohttp.web_exceptions import HTTPNotFound +from aiohttp.web_exceptions import HTTPNotFound, HTTPForbidden from aiohttp.web_urldispatcher import StaticResource -from yarl import URL + +CACHE_TIME = 31 * 86400 # = 1 month +CACHE_HEADERS = {hdrs.CACHE_CONTROL: "public, max-age={}".format(CACHE_TIME)} +# https://github.com/PyCQA/astroid/issues/633 +# pylint: disable=duplicate-bases class CachingStaticResource(StaticResource): """Static Resource handler that will add cache headers.""" async def _handle(self, request): - filename = URL(request.match_info['filename']).path + rel_url = request.match_info['filename'] try: - # PyLint is wrong about resolve not being a member. + filename = Path(rel_url) + if filename.anchor: + # rel_url is an absolute name like + # /static/\\machine_name\c$ or /static/D:\path + # where the static dir is totally different + raise HTTPForbidden() filepath = self._directory.joinpath(filename).resolve() if not self._follow_symlinks: filepath.relative_to(self._directory) @@ -24,30 +35,10 @@ class CachingStaticResource(StaticResource): request.app.logger.exception(error) raise HTTPNotFound() from error + # on opening a dir, load its contents if allowed if filepath.is_dir(): return await super()._handle(request) if filepath.is_file(): - return CachingFileResponse(filepath, chunk_size=self._chunk_size) + return FileResponse( + filepath, chunk_size=self._chunk_size, headers=CACHE_HEADERS) raise HTTPNotFound - - -# pylint: disable=too-many-ancestors -class CachingFileResponse(FileResponse): - """FileSender class that caches output if not in dev mode.""" - - def __init__(self, *args, **kwargs): - """Initialize the hass file sender.""" - super().__init__(*args, **kwargs) - - orig_sendfile = self._sendfile - - async def sendfile(request, fobj, count): - """Sendfile that includes a cache header.""" - cache_time = 31 * 86400 # = 1 month - self.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format( - cache_time) - - await orig_sendfile(request, fobj, count) - - # Overwriting like this because __init__ can change implementation. - self._sendfile = sendfile From 06f2aa93a4c21677209f7c352d9436dcf6fc2f25 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 15 Feb 2019 18:40:46 +0100 Subject: [PATCH 023/253] Add persistent notification --- homeassistant/bootstrap.py | 17 +++++++++++++++++ homeassistant/helpers/config_validation.py | 20 +++++++++++--------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 90a74f23598..7e12a516478 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -192,6 +192,23 @@ async def async_from_config_dict(config: Dict[str, Any], '\n\n'.join(msg), "Config Warning", "config_warning" ) + # TEMP: warn users for invalid slugs + # Remove after 0.92 + if cv.INVALID_EXTRA_KEYS_FOUND: + msg = [] + msg.append( + "Your configuration contains extra keys " + "that the platform does not support (but were silently " + "accepted before 0.88). Please find and remove the following." + "This will become a breaking change." + ) + msg.append('\n'.join('- {}'.format(it) + for it in cv.INVALID_EXTRA_KEYS_FOUND)) + + hass.components.persistent_notification.async_create( + '\n\n'.join(msg), "Config Warning", "config_warning" + ) + return hass diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index f10cf9e057e..41a55dc38dc 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -25,8 +25,6 @@ from homeassistant.helpers import template as template_helper from homeassistant.helpers.logging import KeywordStyleAdapter from homeassistant.util import slugify as util_slugify -_LOGGER = logging.getLogger(__name__) - # pylint: disable=invalid-name TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM' or 'HH:MM:SS'" @@ -36,6 +34,7 @@ OLD_ENTITY_ID_VALIDATION = r"^(\w+)\.(\w+)$" # persistent notification. Rare temporary exception to use a global. INVALID_SLUGS_FOUND = {} INVALID_ENTITY_IDS_FOUND = {} +INVALID_EXTRA_KEYS_FOUND = [] # Home Assistant types @@ -661,14 +660,17 @@ class HASchema(vol.Schema): if err.error_message == 'extra keys not allowed'] if extra_key_errs: msg = "Your configuration contains extra keys " \ - "that the platform does not support. The keys " - msg += ', '.join('[{}]'.format(err.path[-1]) for err in - extra_key_errs) - msg += ' are 42.' + "that the platform does not support.\n" \ + "Please remove " + submsg = ', '.join('[{}]'.format(err.path[-1]) for err in + extra_key_errs) + submsg += '. ' if hasattr(data, '__config_file__'): - msg += " (See {}, line {}). ".format(data.__config_file__, - data.__line__) - _LOGGER.warning(msg) + submsg += " (See {}, line {}). ".format( + data.__config_file__, data.__line__) + msg += submsg + logging.getLogger(__name__).warning(msg) + INVALID_EXTRA_KEYS_FOUND.append(submsg) else: # This should not happen (all errors should be extra key # errors). Let's raise the original error anyway. From 05808afe750278dd6afd7adf130c9c90e46f785e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Feb 2019 09:46:03 -0800 Subject: [PATCH 024/253] Update pychromecast (#21097) --- homeassistant/components/cast/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 94b926795e7..5e6bd720d4b 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -2,7 +2,7 @@ from homeassistant import config_entries from homeassistant.helpers import config_entry_flow -REQUIREMENTS = ['pychromecast==2.5.0'] +REQUIREMENTS = ['pychromecast==2.5.1'] DOMAIN = 'cast' diff --git a/requirements_all.txt b/requirements_all.txt index edd46dfd9a1..3df9fa89d54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -959,7 +959,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==2.5.0 +pychromecast==2.5.1 # homeassistant.components.media_player.cmus pycmus==0.1.1 From f7a6027466befcc749793a3b023c8a7e8bb42540 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 15 Feb 2019 10:08:59 -0800 Subject: [PATCH 025/253] Updated frontend to 20190215.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 be2551457d0..d35514160c9 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190213.0'] +REQUIREMENTS = ['home-assistant-frontend==20190215.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 3df9fa89d54..d4bc232f645 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190213.0 +home-assistant-frontend==20190215.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 50e83ba952b..b6cb2ab41bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -116,7 +116,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190213.0 +home-assistant-frontend==20190215.0 # homeassistant.components.homekit_controller homekit==0.12.2 From 3a6a2467461b0a79ea72285909286750e09ac1f5 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 15 Feb 2019 13:14:58 -0500 Subject: [PATCH 026/253] Set ZHA device availability on new join (#21066) * set availability on device join * fix new join test --- homeassistant/components/zha/core/gateway.py | 3 +++ tests/components/zha/common.py | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 02ed1d73699..ff3c374a850 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -164,6 +164,9 @@ class ZHAGateway: device_entity = _create_device_entity(zha_device) await self._component.async_add_entities([device_entity]) + if is_new_join: + zha_device.update_available(True) + async def _async_process_endpoint( self, endpoint_id, endpoint, discovery_infos, device, zha_device, is_new_join): diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 1a923849ce5..f0e1aa701e7 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -1,7 +1,6 @@ """Common test objects.""" import time from unittest.mock import patch, Mock -from homeassistant.const import STATE_UNAVAILABLE from homeassistant.components.zha.core.helpers import convert_ieee from homeassistant.components.zha.core.const import ( DATA_ZHA, DATA_ZHA_CONFIG, DATA_ZHA_DISPATCHERS, DATA_ZHA_BRIDGE_ID @@ -191,4 +190,4 @@ async def async_test_device_join( cluster = zigpy_device.endpoints.get(1).in_clusters[cluster_id] entity_id = make_entity_id( domain, zigpy_device, cluster, use_suffix=device_type is None) - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + assert hass.states.get(entity_id) is not None From 9203ae201f94323d89996bcccf2d9e36753db851 Mon Sep 17 00:00:00 2001 From: Phil Hawthorne Date: Sat, 16 Feb 2019 05:25:03 +1100 Subject: [PATCH 027/253] Set uvloop version consistent with hass.io (#21080) This sets the uvloop version in Docker containers to 0.11.3, which is the same version that hass.io uses. uvloop might be causing issues with some Docker containers on some host systems, as reported in #20829 --- Dockerfile | 2 +- virtualization/Docker/Dockerfile.dev | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c863ff9433c..aa9415fd1e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ COPY requirements_all.txt requirements_all.txt # Uninstall enum34 because some dependencies install it but breaks Python 3.4+. # See PR #8103 for more info. RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet cython tensorflow + pip3 install --no-cache-dir mysqlclient psycopg2 uvloop==0.11.3 cchardet cython tensorflow # Copy source COPY . . diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index de460319bc2..03d6ab47c24 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -29,7 +29,7 @@ COPY requirements_all.txt requirements_all.txt # Uninstall enum34 because some dependencies install it but breaks Python 3.4+. # See PR #8103 for more info. RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet cython + pip3 install --no-cache-dir mysqlclient psycopg2 uvloop==0.11.3 cchardet cython # BEGIN: Development additions From 539d24dd60d7d0d17ddc287995ef57f728d8eeab Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 15 Feb 2019 11:28:23 -0700 Subject: [PATCH 028/253] Bump aioambient to 0.1.2 (#21098) --- homeassistant/components/ambient_station/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 5972660c6e6..4a7864d3f7f 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -20,7 +20,7 @@ from .const import ( ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, TYPE_BINARY_SENSOR, TYPE_SENSOR) -REQUIREMENTS = ['aioambient==0.1.1'] +REQUIREMENTS = ['aioambient==0.1.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d4bc232f645..b7f4a207296 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -90,7 +90,7 @@ abodepy==0.15.0 afsapi==0.0.4 # homeassistant.components.ambient_station -aioambient==0.1.1 +aioambient==0.1.2 # homeassistant.components.asuswrt aioasuswrt==1.1.20 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6cb2ab41bc..cbcb4676547 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -31,7 +31,7 @@ PyTransportNSW==0.1.1 YesssSMS==0.2.3 # homeassistant.components.ambient_station -aioambient==0.1.1 +aioambient==0.1.2 # homeassistant.components.device_tracker.automatic aioautomatic==0.6.5 From 4509caefde4829327fecb45b5aded0938c2f8c0b Mon Sep 17 00:00:00 2001 From: David Barrera Date: Sat, 16 Feb 2019 03:29:24 -0500 Subject: [PATCH 029/253] Add index parameter to scrape sensor (#21084) * Add index parameter to scrape sensor The scrape sensor selects the first element of the list returned by BeautifulSoup. This commit adds an optional index parameter to allow the selection of a different element from the list of results. To make this a non-breaking change, if no index value is configured, the sensor defaults to the previous behaviour of returning the first element. * Set default value for index to avoid later checks --- homeassistant/components/sensor/scrape.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/scrape.py b/homeassistant/components/sensor/scrape.py index 6dd52789f71..70dfae392be 100644 --- a/homeassistant/components/sensor/scrape.py +++ b/homeassistant/components/sensor/scrape.py @@ -26,6 +26,7 @@ _LOGGER = logging.getLogger(__name__) CONF_ATTR = 'attribute' CONF_SELECT = 'select' +CONF_INDEX = 'index' DEFAULT_NAME = 'Web scrape' DEFAULT_VERIFY_SSL = True @@ -34,6 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.string, vol.Required(CONF_SELECT): cv.string, vol.Optional(CONF_ATTR): cv.string, + vol.Optional(CONF_INDEX, default=0): cv.positive_int, vol.Optional(CONF_AUTHENTICATION): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), @@ -56,6 +58,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): verify_ssl = config.get(CONF_VERIFY_SSL) select = config.get(CONF_SELECT) attr = config.get(CONF_ATTR) + index = config.get(CONF_INDEX) unit = config.get(CONF_UNIT_OF_MEASUREMENT) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -77,19 +80,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): raise PlatformNotReady add_entities([ - ScrapeSensor(rest, name, select, attr, value_template, unit)], True) + ScrapeSensor(rest, name, select, attr, index, value_template, unit)], + True) class ScrapeSensor(Entity): """Representation of a web scrape sensor.""" - def __init__(self, rest, name, select, attr, value_template, unit): + def __init__(self, rest, name, select, attr, index, value_template, unit): """Initialize a web scrape sensor.""" self.rest = rest self._name = name self._state = None self._select = select self._attr = attr + self._index = index self._value_template = value_template self._unit_of_measurement = unit @@ -119,9 +124,9 @@ class ScrapeSensor(Entity): try: if self._attr is not None: - value = raw_data.select(self._select)[0][self._attr] + value = raw_data.select(self._select)[self._index][self._attr] else: - value = raw_data.select(self._select)[0].text + value = raw_data.select(self._select)[self._index].text _LOGGER.debug(value) except IndexError: _LOGGER.error("Unable to extract data from HTML") From 4c23ccad983e44d3cac7104c4f11434d744dda24 Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Sat, 16 Feb 2019 03:12:16 -0600 Subject: [PATCH 030/253] Owlet baby monitor component (#21108) --- .coveragerc | 1 + homeassistant/components/owlet/__init__.py | 70 ++++++++++++ .../components/owlet/binary_sensor.py | 82 ++++++++++++++ homeassistant/components/owlet/const.py | 6 + homeassistant/components/owlet/sensor.py | 103 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 265 insertions(+) create mode 100644 homeassistant/components/owlet/__init__.py create mode 100644 homeassistant/components/owlet/binary_sensor.py create mode 100644 homeassistant/components/owlet/const.py create mode 100644 homeassistant/components/owlet/sensor.py diff --git a/.coveragerc b/.coveragerc index dec0594e7a3..8b51a34df61 100644 --- a/.coveragerc +++ b/.coveragerc @@ -376,6 +376,7 @@ omit = homeassistant/components/openuv/__init__.py homeassistant/components/openuv/binary_sensor.py homeassistant/components/openuv/sensor.py + homeassistant/components/owlet/* homeassistant/components/pilight/* homeassistant/components/plum_lightpad/* homeassistant/components/point/* diff --git a/homeassistant/components/owlet/__init__.py b/homeassistant/components/owlet/__init__.py new file mode 100644 index 00000000000..c29f937183f --- /dev/null +++ b/homeassistant/components/owlet/__init__.py @@ -0,0 +1,70 @@ +"""Support for Owlet baby monitors.""" +import logging + +import voluptuous as vol + +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, CONF_NAME) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +from .const import SENSOR_MOVEMENT, SENSOR_BASE_STATION, SENSOR_HEART_RATE, \ + SENSOR_OXYGEN_LEVEL + +REQUIREMENTS = ['pyowlet==1.0.2'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'owlet' + +SENSOR_TYPES = [ + SENSOR_OXYGEN_LEVEL, + SENSOR_HEART_RATE, + SENSOR_BASE_STATION, + SENSOR_MOVEMENT +] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up owlet component.""" + from pyowlet.PyOwlet import PyOwlet + + username = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] + name = config[DOMAIN].get(CONF_NAME) + + try: + device = PyOwlet(username, password) + except KeyError: + _LOGGER.error('Owlet authentication failed. Please verify your ' + 'credentials are correct.') + return False + + device.update_properties() + + if not name: + name = '{}\'s Owlet'.format(device.baby_name) + + hass.data[DOMAIN] = OwletDevice(device, name, SENSOR_TYPES) + + load_platform(hass, 'sensor', DOMAIN, {}, config) + load_platform(hass, 'binary_sensor', DOMAIN, {}, config) + + return True + + +class OwletDevice(): + """Represents a configured Owlet device.""" + + def __init__(self, device, name, monitor): + """Initialize device.""" + self.name = name + self.monitor = monitor + self.device = device diff --git a/homeassistant/components/owlet/binary_sensor.py b/homeassistant/components/owlet/binary_sensor.py new file mode 100644 index 00000000000..cb66278150a --- /dev/null +++ b/homeassistant/components/owlet/binary_sensor.py @@ -0,0 +1,82 @@ +"""Support for Owlet binary sensors.""" +from datetime import timedelta + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.owlet import DOMAIN as OWLET_DOMAIN +from homeassistant.util import dt as dt_util + +from .const import SENSOR_BASE_STATION, SENSOR_MOVEMENT + +SCAN_INTERVAL = timedelta(seconds=120) + +BINARY_CONDITIONS = { + SENSOR_BASE_STATION: { + 'name': 'Base Station', + 'device_class': 'power' + }, + SENSOR_MOVEMENT: { + 'name': 'Movement', + 'device_class': 'motion' + } +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up owlet binary sensor.""" + if discovery_info is None: + return + + device = hass.data[OWLET_DOMAIN] + + entities = [] + for condition in BINARY_CONDITIONS: + if condition in device.monitor: + entities.append(OwletBinarySensor(device, condition)) + + add_entities(entities, True) + + +class OwletBinarySensor(BinarySensorDevice): + """Representation of owlet binary sensor.""" + + def __init__(self, device, condition): + """Init owlet binary sensor.""" + self._device = device + self._condition = condition + self._state = None + self._base_on = False + self._prop_expiration = None + self._is_charging = None + + @property + def name(self): + """Return sensor name.""" + return '{} {}'.format(self._device.name, + BINARY_CONDITIONS[self._condition]['name']) + + @property + def is_on(self): + """Return current state of sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class.""" + return BINARY_CONDITIONS[self._condition]['device_class'] + + def update(self): + """Update state of sensor.""" + self._base_on = self._device.device.base_station_on + self._prop_expiration = self._device.device.prop_expire_time + self._is_charging = self._device.device.charge_status > 0 + + # handle expired values + if self._prop_expiration < dt_util.now().timestamp(): + self._state = False + return + + if self._condition == 'movement': + if not self._base_on or self._is_charging: + return False + + self._state = getattr(self._device.device, self._condition) diff --git a/homeassistant/components/owlet/const.py b/homeassistant/components/owlet/const.py new file mode 100644 index 00000000000..f8d4db3ec1e --- /dev/null +++ b/homeassistant/components/owlet/const.py @@ -0,0 +1,6 @@ +"""Constants for Owlet component.""" +SENSOR_OXYGEN_LEVEL = 'oxygen_level' +SENSOR_HEART_RATE = 'heart_rate' + +SENSOR_BASE_STATION = 'base_station_on' +SENSOR_MOVEMENT = 'movement' diff --git a/homeassistant/components/owlet/sensor.py b/homeassistant/components/owlet/sensor.py new file mode 100644 index 00000000000..b91cc387718 --- /dev/null +++ b/homeassistant/components/owlet/sensor.py @@ -0,0 +1,103 @@ +"""Support for Owlet sensors.""" +from datetime import timedelta + +from homeassistant.components.owlet import DOMAIN as OWLET_DOMAIN +from homeassistant.helpers.entity import Entity +from homeassistant.util import dt as dt_util + +from .const import SENSOR_HEART_RATE, SENSOR_OXYGEN_LEVEL + +SCAN_INTERVAL = timedelta(seconds=120) + +SENSOR_CONDITIONS = { + SENSOR_OXYGEN_LEVEL: { + 'name': 'Oxygen Level', + 'device_class': None + }, + SENSOR_HEART_RATE: { + 'name': 'Heart Rate', + 'device_class': None + } +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up owlet binary sensor.""" + if discovery_info is None: + return + + device = hass.data[OWLET_DOMAIN] + + entities = [] + for condition in SENSOR_CONDITIONS: + if condition in device.monitor: + entities.append(OwletSensor(device, condition)) + + add_entities(entities, True) + + +class OwletSensor(Entity): + """Representation of Owlet sensor.""" + + def __init__(self, device, condition): + """Init owlet binary sensor.""" + self._device = device + self._condition = condition + self._state = None + self._prop_expiration = None + self.is_charging = None + self.battery_level = None + self.sock_off = None + self.sock_connection = None + self._movement = None + + @property + def name(self): + """Return sensor name.""" + return '{} {}'.format(self._device.name, + SENSOR_CONDITIONS[self._condition]['name']) + + @property + def state(self): + """Return current state of sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class.""" + return SENSOR_CONDITIONS[self._condition]['device_class'] + + @property + def device_state_attributes(self): + """Return state attributes.""" + attributes = { + 'battery_charging': self.is_charging, + 'battery_level': self.battery_level, + 'sock_off': self.sock_off, + 'sock_connection': self.sock_connection + } + + return attributes + + def update(self): + """Update state of sensor.""" + self.is_charging = self._device.device.charge_status + self.battery_level = self._device.device.batt_level + self.sock_off = self._device.device.sock_off + self.sock_connection = self._device.device.sock_connection + self._movement = self._device.device.movement + self._prop_expiration = self._device.device.prop_expire_time + + value = getattr(self._device.device, self._condition) + + if self._condition == 'batt_level': + self._state = min(100, value) + return + + if not self._device.device.base_station_on \ + or self._device.device.charge_status > 0 \ + or self._prop_expiration < dt_util.now().timestamp() \ + or self._movement: + value = None + + self._state = value diff --git a/requirements_all.txt b/requirements_all.txt index b7f4a207296..7773f3bbbd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1188,6 +1188,9 @@ pyotgw==0.4b1 # homeassistant.components.sensor.otp pyotp==2.2.6 +# homeassistant.components.owlet +pyowlet==1.0.2 + # homeassistant.components.sensor.openweathermap # homeassistant.components.weather.openweathermap pyowm==2.10.0 From b39b66ef65c23dcc3fff195d9a9c015d0d29685e Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sat, 16 Feb 2019 03:49:24 -0600 Subject: [PATCH 031/253] Fix SmartThings Translation Error (#21103) --- homeassistant/components/smartthings/.translations/en.json | 2 +- homeassistant/components/smartthings/strings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smartthings/.translations/en.json b/homeassistant/components/smartthings/.translations/en.json index 2091ddb00a2..e35035b8fa0 100644 --- a/homeassistant/components/smartthings/.translations/en.json +++ b/homeassistant/components/smartthings/.translations/en.json @@ -8,7 +8,7 @@ "token_forbidden": "The token does not have the required OAuth scopes.", "token_invalid_format": "The token must be in the UID/GUID format", "token_unauthorized": "The token is invalid or no longer authorized.", - "webhook_error": "SmartThings could not validate the endpoint configured in `base_url`. Please review the [component requirements]({component_url})." + "webhook_error": "SmartThings could not validate the endpoint configured in `base_url`. Please review the component requirements." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index bcbe02f6011..3578bcd5138 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -22,7 +22,7 @@ "app_setup_error": "Unable to setup the SmartApp. Please try again.", "app_not_installed": "Please ensure you have installed and authorized the Home Assistant SmartApp and try again.", "base_url_not_https": "The `base_url` for the `http` component must be configured and start with `https://`.", - "webhook_error": "SmartThings could not validate the endpoint configured in `base_url`. Please review the [component requirements]({component_url})." + "webhook_error": "SmartThings could not validate the endpoint configured in `base_url`. Please review the component requirements." } } } \ No newline at end of file From f7d903148653d8a595a309b2cb629840e923fa2e Mon Sep 17 00:00:00 2001 From: Nick Horvath Date: Sat, 16 Feb 2019 05:18:13 -0500 Subject: [PATCH 032/253] Bump thermoworks_smoke version to get new pyrebase version (#21100) --- homeassistant/components/sensor/thermoworks_smoke.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/thermoworks_smoke.py b/homeassistant/components/sensor/thermoworks_smoke.py index e81a3974176..0c6cddd9fcd 100644 --- a/homeassistant/components/sensor/thermoworks_smoke.py +++ b/homeassistant/components/sensor/thermoworks_smoke.py @@ -17,7 +17,7 @@ from homeassistant.const import TEMP_FAHRENHEIT, CONF_EMAIL, CONF_PASSWORD,\ CONF_MONITORED_CONDITIONS, CONF_EXCLUDE, ATTR_BATTERY_LEVEL from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['thermoworks_smoke==0.1.7', 'stringcase==1.2.0'] +REQUIREMENTS = ['thermoworks_smoke==0.1.8', 'stringcase==1.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7773f3bbbd1..308ee2f0a28 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1662,7 +1662,7 @@ temperusb==1.5.3 teslajsonpy==0.0.23 # homeassistant.components.sensor.thermoworks_smoke -thermoworks_smoke==0.1.7 +thermoworks_smoke==0.1.8 # homeassistant.components.thingspeak thingspeak==0.4.1 From a49686879da6a0286a30ab9fc5870461cebaf447 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 16 Feb 2019 14:51:30 +0100 Subject: [PATCH 033/253] Update bootstrap.py --- homeassistant/bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 7e12a516478..a018d540033 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -192,7 +192,7 @@ async def async_from_config_dict(config: Dict[str, Any], '\n\n'.join(msg), "Config Warning", "config_warning" ) - # TEMP: warn users for invalid slugs + # TEMP: warn users of invalid extra keys # Remove after 0.92 if cv.INVALID_EXTRA_KEYS_FOUND: msg = [] From 1a6c79d5e2dcbfb2cd863968e5f89169768cf6a2 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 16 Feb 2019 17:03:08 +0100 Subject: [PATCH 034/253] Order imports (#21117) --- homeassistant/components/owlet/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/owlet/__init__.py b/homeassistant/components/owlet/__init__.py index c29f937183f..b7ad7ab9152 100644 --- a/homeassistant/components/owlet/__init__.py +++ b/homeassistant/components/owlet/__init__.py @@ -3,12 +3,13 @@ import logging import voluptuous as vol -from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, CONF_NAME) +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -from .const import SENSOR_MOVEMENT, SENSOR_BASE_STATION, SENSOR_HEART_RATE, \ - SENSOR_OXYGEN_LEVEL +from .const import ( + SENSOR_BASE_STATION, SENSOR_HEART_RATE, SENSOR_MOVEMENT, + SENSOR_OXYGEN_LEVEL) REQUIREMENTS = ['pyowlet==1.0.2'] @@ -20,7 +21,7 @@ SENSOR_TYPES = [ SENSOR_OXYGEN_LEVEL, SENSOR_HEART_RATE, SENSOR_BASE_STATION, - SENSOR_MOVEMENT + SENSOR_MOVEMENT, ] CONFIG_SCHEMA = vol.Schema({ @@ -43,8 +44,8 @@ def setup(hass, config): try: device = PyOwlet(username, password) except KeyError: - _LOGGER.error('Owlet authentication failed. Please verify your ' - 'credentials are correct.') + _LOGGER.error("Owlet authentication failed. Please verify your " + "credentials are correct") return False device.update_properties() From 66267474dcd4e666d0df8de3f9911b1319ee9c40 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Feb 2019 12:00:04 -0800 Subject: [PATCH 035/253] Updated frontend to 20190216.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 d35514160c9..3db63e65a6d 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190215.0'] +REQUIREMENTS = ['home-assistant-frontend==20190216.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 308ee2f0a28..f80b8652589 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190215.0 +home-assistant-frontend==20190216.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cbcb4676547..40e7a37326d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -116,7 +116,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190215.0 +home-assistant-frontend==20190216.0 # homeassistant.components.homekit_controller homekit==0.12.2 From 451241b48102ede9156f0ba16a46d6a3b9469b63 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sat, 16 Feb 2019 15:43:30 -0800 Subject: [PATCH 036/253] Update Lakeside dependency in Eufy component This should fix #16834 --- homeassistant/components/eufy/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py index d5a0938bf66..b0bd9109363 100644 --- a/homeassistant/components/eufy/__init__.py +++ b/homeassistant/components/eufy/__init__.py @@ -9,7 +9,7 @@ from homeassistant.const import ( from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['lakeside==0.11'] +REQUIREMENTS = ['lakeside==0.12'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f80b8652589..08632575d6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -606,7 +606,7 @@ kiwiki-client==0.1.1 konnected==0.1.4 # homeassistant.components.eufy -lakeside==0.11 +lakeside==0.12 # homeassistant.components.owntracks libnacl==1.6.1 From ea592a003be2c38b2d7417acdad422f2b9e80555 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Feb 2019 17:48:43 -0800 Subject: [PATCH 037/253] Bump pychromecast to 2.5.2 (#21127) --- homeassistant/components/cast/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 5e6bd720d4b..1b3da200540 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -2,7 +2,7 @@ from homeassistant import config_entries from homeassistant.helpers import config_entry_flow -REQUIREMENTS = ['pychromecast==2.5.1'] +REQUIREMENTS = ['pychromecast==2.5.2'] DOMAIN = 'cast' diff --git a/requirements_all.txt b/requirements_all.txt index f80b8652589..d038d5edf12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -959,7 +959,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==2.5.1 +pychromecast==2.5.2 # homeassistant.components.media_player.cmus pycmus==0.1.1 From 9c92880b5a713016912af67e7dbe0cf651ceafb4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Feb 2019 19:38:52 -0800 Subject: [PATCH 038/253] Handle ValueError (#21126) --- homeassistant/components/person/__init__.py | 30 +++++++++++++-------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 63e588f911b..6fb7d42e0ee 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -247,7 +247,7 @@ class PersonManager: if any(person for person in chain(self.storage_data.values(), self.config_data.values()) - if person[CONF_USER_ID] == user_id): + if person.get(CONF_USER_ID) == user_id): raise ValueError("User already taken") async def _user_removed(self, event: Event): @@ -417,7 +417,7 @@ def ws_list_person(hass: HomeAssistantType, @websocket_api.websocket_command({ vol.Required('type'): 'person/create', - vol.Required('name'): str, + vol.Required('name'): vol.All(str, vol.Length(min=1)), vol.Optional('user_id'): vol.Any(str, None), vol.Optional('device_trackers', default=[]): vol.All( cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN)), @@ -428,18 +428,22 @@ async def ws_create_person(hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg): """Create a person.""" manager = hass.data[DOMAIN] # type: PersonManager - person = await manager.async_create_person( - name=msg['name'], - user_id=msg.get('user_id'), - device_trackers=msg['device_trackers'] - ) - connection.send_result(msg['id'], person) + try: + person = await manager.async_create_person( + name=msg['name'], + user_id=msg.get('user_id'), + device_trackers=msg['device_trackers'] + ) + connection.send_result(msg['id'], person) + except ValueError as err: + connection.send_error( + msg['id'], websocket_api.const.ERR_INVALID_FORMAT, str(err)) @websocket_api.websocket_command({ vol.Required('type'): 'person/update', vol.Required('person_id'): str, - vol.Optional('name'): str, + vol.Required('name'): vol.All(str, vol.Length(min=1)), vol.Optional('user_id'): vol.Any(str, None), vol.Optional(CONF_DEVICE_TRACKERS, default=[]): vol.All( cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN)), @@ -455,8 +459,12 @@ async def ws_update_person(hass: HomeAssistantType, if key in msg: changes[key] = msg[key] - person = await manager.async_update_person(msg['person_id'], **changes) - connection.send_result(msg['id'], person) + try: + person = await manager.async_update_person(msg['person_id'], **changes) + connection.send_result(msg['id'], person) + except ValueError as err: + connection.send_error( + msg['id'], websocket_api.const.ERR_INVALID_FORMAT, str(err)) @websocket_api.websocket_command({ From 9cab597bc43c44d024b784fa9c2c825ec767b326 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 17 Feb 2019 04:04:56 +0000 Subject: [PATCH 039/253] Don't expose services in Utility_Meter unless tariffs are available (#20878) * only expose services when tariffs configured * don't register services multiple times --- .../components/utility_meter/__init__.py | 28 +++++++++++-------- .../components/utility_meter/sensor.py | 1 + 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 7d8e4ddf71b..3cf1b2fea61 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -51,6 +51,7 @@ async def async_setup(hass, config): """Set up an Utility Meter.""" component = EntityComponent(_LOGGER, DOMAIN, hass) hass.data[DATA_UTILITY] = {} + register_services = False for meter, conf in config.get(DOMAIN).items(): _LOGGER.debug("Setup %s.%s", DOMAIN, meter) @@ -80,21 +81,23 @@ async def async_setup(hass, config): }) hass.async_create_task(discovery.async_load_platform( hass, SENSOR_DOMAIN, DOMAIN, tariff_confs, config)) + register_services = True - component.async_register_entity_service( - SERVICE_RESET, SERVICE_METER_SCHEMA, - 'async_reset_meters' - ) + if register_services: + component.async_register_entity_service( + SERVICE_RESET, SERVICE_METER_SCHEMA, + 'async_reset_meters' + ) - component.async_register_entity_service( - SERVICE_SELECT_TARIFF, SERVICE_SELECT_TARIFF_SCHEMA, - 'async_select_tariff' - ) + component.async_register_entity_service( + SERVICE_SELECT_TARIFF, SERVICE_SELECT_TARIFF_SCHEMA, + 'async_select_tariff' + ) - component.async_register_entity_service( - SERVICE_SELECT_NEXT_TARIFF, SERVICE_METER_SCHEMA, - 'async_next_tariff' - ) + component.async_register_entity_service( + SERVICE_SELECT_NEXT_TARIFF, SERVICE_METER_SCHEMA, + 'async_next_tariff' + ) return True @@ -150,6 +153,7 @@ class TariffSelect(RestoreEntity): async def async_reset_meters(self): """Reset all sensors of this meter.""" + _LOGGER.debug("reset meter %s", self.entity_id) async_dispatcher_send(self.hass, SIGNAL_RESET_METER, self.entity_id) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index d3edf7d501b..a59d51d97e2 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -189,6 +189,7 @@ class UtilityMeterSensor(RestoreEntity): if self._tariff != tariff_entity_state.state: return + _LOGGER.debug("tracking source: %s", self._sensor_source_id) self._collecting = async_track_state_change( self.hass, self._sensor_source_id, self.async_reading) From 481439d387a5f7c3cf3876c0ab3ad2adab16d0f7 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sat, 16 Feb 2019 21:23:09 -0800 Subject: [PATCH 040/253] Deprecate conf_update_interval (#20924) * Deprecate update_interval and replace with scan_interval * Update tests * Fix Darksky tests * Fix Darksky tests correctly This reverts commit a73384a223ba8a93c682042d9351cd5a7a399183. * Provide the default for the non deprecated option * Don't override default schema for sensors --- .../components/fastdotcom/__init__.py | 27 ++++++--- homeassistant/components/freedns/__init__.py | 7 ++- .../components/mythicbeastsdns/__init__.py | 30 +++++++--- homeassistant/components/sensor/broadlink.py | 34 +++++++---- homeassistant/components/sensor/darksky.py | 57 ++++++++++++------- homeassistant/components/sensor/fedex.py | 30 +++++++--- homeassistant/components/sensor/ups.py | 35 ++++++++---- .../components/speedtestdotnet/__init__.py | 35 ++++++++---- .../components/tellduslive/__init__.py | 27 +++++---- .../components/volvooncall/__init__.py | 43 ++++++++------ homeassistant/const.py | 4 ++ homeassistant/helpers/config_validation.py | 3 +- tests/components/freedns/test_init.py | 4 +- tests/components/sensor/test_darksky.py | 15 +++-- tests/helpers/test_config_validation.py | 18 +++++- 15 files changed, 249 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index a63fab76861..2e092e527c5 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -5,7 +5,8 @@ from datetime import timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_UPDATE_INTERVAL +from homeassistant.const import CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL, \ + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -22,13 +23,21 @@ CONF_MANUAL = 'manual' DEFAULT_INTERVAL = timedelta(hours=1) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): - vol.All( - cv.time_period, cv.positive_timedelta - ), - vol.Optional(CONF_MANUAL, default=False): cv.boolean, - }) + DOMAIN: vol.All( + vol.Schema({ + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_MANUAL, default=False): cv.boolean, + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=DEFAULT_INTERVAL + ) + ) }, extra=vol.ALLOW_EXTRA) @@ -39,7 +48,7 @@ async def async_setup(hass, config): if not conf[CONF_MANUAL]: async_track_time_interval( - hass, data.update, conf[CONF_UPDATE_INTERVAL] + hass, data.update, conf[CONF_SCAN_INTERVAL] ) def update(call=None): diff --git a/homeassistant/components/freedns/__init__.py b/homeassistant/components/freedns/__init__.py index 7da51cd42e4..edb3a57c28c 100644 --- a/homeassistant/components/freedns/__init__.py +++ b/homeassistant/components/freedns/__init__.py @@ -13,7 +13,8 @@ import async_timeout import voluptuous as vol from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN, - CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL) + CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL, + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -32,13 +33,13 @@ CONFIG_SCHEMA = vol.Schema({ vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string, vol.Optional(CONF_UPDATE_INTERVAL): vol.All(cv.time_period, cv.positive_timedelta), - vol.Optional(CONF_SCAN_INTERVAL): + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): vol.All(cv.time_period, cv.positive_timedelta), }), cv.deprecated( CONF_UPDATE_INTERVAL, replacement_key=CONF_SCAN_INTERVAL, - invalidation_version='1.0.0', + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, default=DEFAULT_INTERVAL ) ) diff --git a/homeassistant/components/mythicbeastsdns/__init__.py b/homeassistant/components/mythicbeastsdns/__init__.py index f34b2736710..3d0d250557b 100644 --- a/homeassistant/components/mythicbeastsdns/__init__.py +++ b/homeassistant/components/mythicbeastsdns/__init__.py @@ -5,7 +5,9 @@ import logging import voluptuous as vol from homeassistant.const import ( - CONF_DOMAIN, CONF_HOST, CONF_PASSWORD, CONF_UPDATE_INTERVAL) + CONF_HOST, CONF_DOMAIN, CONF_PASSWORD, CONF_UPDATE_INTERVAL, + CONF_SCAN_INTERVAL, CONF_UPDATE_INTERVAL_INVALIDATION_VERSION +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval @@ -19,13 +21,23 @@ DOMAIN = 'mythicbeastsdns' DEFAULT_INTERVAL = timedelta(minutes=10) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_DOMAIN): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): vol.All( - cv.time_period, cv.positive_timedelta), - }) + DOMAIN: vol.All( + vol.Schema({ + vol.Required(CONF_DOMAIN): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=DEFAULT_INTERVAL + ) + ) }, extra=vol.ALLOW_EXTRA) @@ -36,7 +48,7 @@ async def async_setup(hass, config): domain = config[DOMAIN][CONF_DOMAIN] password = config[DOMAIN][CONF_PASSWORD] host = config[DOMAIN][CONF_HOST] - update_interval = config[DOMAIN][CONF_UPDATE_INTERVAL] + update_interval = config[DOMAIN][CONF_SCAN_INTERVAL] session = async_get_clientsession(hass) diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index 50f9f955148..5720201b3f2 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -14,7 +14,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_CELSIUS, - CONF_TIMEOUT, CONF_UPDATE_INTERVAL) + CONF_TIMEOUT, CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL, + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -25,6 +26,7 @@ _LOGGER = logging.getLogger(__name__) DEVICE_DEFAULT_NAME = 'Broadlink sensor' DEFAULT_TIMEOUT = 10 +SCAN_INTERVAL = timedelta(seconds=300) SENSOR_TYPES = { 'temperature': ['Temperature', TEMP_CELSIUS], @@ -34,16 +36,24 @@ SENSOR_TYPES = { 'noise': ['Noise', ' '], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): vol.Coerce(str), - vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=300)): ( - vol.All(cv.time_period, cv.positive_timedelta)), - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int -}) +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): vol.Coerce(str), + vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=SCAN_INTERVAL + ) +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -53,7 +63,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): mac_addr = binascii.unhexlify(mac) name = config.get(CONF_NAME) timeout = config.get(CONF_TIMEOUT) - update_interval = config.get(CONF_UPDATE_INTERVAL) + update_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) broadlink_data = BroadlinkData(update_interval, host, mac_addr, timeout) dev = [] for variable in config[CONF_MONITORED_CONDITIONS]: diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 6e2ca2dc6c5..c68bb2cd3a3 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -14,7 +14,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - CONF_MONITORED_CONDITIONS, CONF_NAME, UNIT_UV_INDEX, CONF_UPDATE_INTERVAL) + CONF_MONITORED_CONDITIONS, CONF_NAME, UNIT_UV_INDEX, CONF_UPDATE_INTERVAL, + CONF_SCAN_INTERVAL, CONF_UPDATE_INTERVAL_INVALIDATION_VERSION) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -30,8 +31,8 @@ CONF_LANGUAGE = 'language' CONF_UNITS = 'units' DEFAULT_LANGUAGE = 'en' - DEFAULT_NAME = 'Dark Sky' +SCAN_INTERVAL = timedelta(seconds=300) DEPRECATED_SENSOR_TYPES = { 'apparent_temperature_max', @@ -167,23 +168,39 @@ LANGUAGE_CODES = [ 'tet', 'tr', 'uk', 'x-pig-latin', 'zh', 'zh-tw', ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']), - vol.Optional(CONF_LANGUAGE, - default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), - vol.Inclusive(CONF_LATITUDE, 'coordinates', - 'Latitude and longitude must exist together'): cv.latitude, - vol.Inclusive(CONF_LONGITUDE, 'coordinates', - 'Latitude and longitude must exist together'): cv.longitude, - vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=300)): ( - vol.All(cv.time_period, cv.positive_timedelta)), - vol.Optional(CONF_FORECAST): - vol.All(cv.ensure_list, [vol.Range(min=0, max=7)]), -}) +ALLOWED_UNITS = ['auto', 'si', 'us', 'ca', 'uk', 'uk2'] + +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNITS): vol.In(ALLOWED_UNITS), + vol.Optional(CONF_LANGUAGE, + default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), + vol.Inclusive( + CONF_LATITUDE, + 'coordinates', + 'Latitude and longitude must exist together' + ): cv.latitude, + vol.Inclusive( + CONF_LONGITUDE, + 'coordinates', + 'Latitude and longitude must exist together' + ): cv.longitude, + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_FORECAST): + vol.All(cv.ensure_list, [vol.Range(min=0, max=7)]), + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=SCAN_INTERVAL + ) +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -191,7 +208,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) language = config.get(CONF_LANGUAGE) - interval = config.get(CONF_UPDATE_INTERVAL) + interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) if CONF_UNITS in config: units = config[CONF_UNITS] diff --git a/homeassistant/components/sensor/fedex.py b/homeassistant/components/sensor/fedex.py index 02938ff837b..54c319e6441 100644 --- a/homeassistant/components/sensor/fedex.py +++ b/homeassistant/components/sensor/fedex.py @@ -12,7 +12,9 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import (CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - ATTR_ATTRIBUTION, CONF_UPDATE_INTERVAL) + ATTR_ATTRIBUTION, CONF_UPDATE_INTERVAL, + CONF_SCAN_INTERVAL, + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION) from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from homeassistant.util import Throttle @@ -31,13 +33,23 @@ ICON = 'mdi:package-variant-closed' STATUS_DELIVERED = 'delivered' -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=1800)): - vol.All(cv.time_period, cv.positive_timedelta), -}) +SCAN_INTERVAL = timedelta(seconds=1800) + +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=SCAN_INTERVAL + ) +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -45,7 +57,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): import fedexdeliverymanager name = config.get(CONF_NAME) - update_interval = config.get(CONF_UPDATE_INTERVAL) + update_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) try: cookie = hass.config.path(COOKIE) diff --git a/homeassistant/components/sensor/ups.py b/homeassistant/components/sensor/ups.py index 44ecdc433c5..e4aab555050 100644 --- a/homeassistant/components/sensor/ups.py +++ b/homeassistant/components/sensor/ups.py @@ -12,7 +12,9 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import (CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - ATTR_ATTRIBUTION, CONF_UPDATE_INTERVAL) + ATTR_ATTRIBUTION, CONF_UPDATE_INTERVAL, + CONF_SCAN_INTERVAL, + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION) from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from homeassistant.util import Throttle @@ -28,13 +30,23 @@ COOKIE = 'upsmychoice_cookies.pickle' ICON = 'mdi:package-variant-closed' STATUS_DELIVERED = 'delivered' -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=1800)): ( - vol.All(cv.time_period, cv.positive_timedelta)), -}) +SCAN_INTERVAL = timedelta(seconds=1800) + +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL): ( + vol.All(cv.time_period, cv.positive_timedelta)), + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=SCAN_INTERVAL + ) +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -49,8 +61,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.exception("Could not connect to UPS My Choice") return False - add_entities([UPSSensor(session, config.get(CONF_NAME), - config.get(CONF_UPDATE_INTERVAL))], True) + add_entities([UPSSensor( + session, + config.get(CONF_NAME), + config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + )], True) class UPSSensor(Entity): diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 3b8d2964f83..4eae738b0d3 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -8,7 +8,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.speedtestdotnet.const import DOMAIN, \ DATA_UPDATED, SENSOR_TYPES from homeassistant.const import CONF_MONITORED_CONDITIONS, \ - CONF_UPDATE_INTERVAL + CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL, \ + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -24,16 +25,26 @@ CONF_MANUAL = 'manual' DEFAULT_INTERVAL = timedelta(hours=1) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_SERVER_ID): cv.positive_int, - vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): - vol.All( - cv.time_period, cv.positive_timedelta - ), - vol.Optional(CONF_MANUAL, default=False): cv.boolean, - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): - vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]) - }) + DOMAIN: vol.All( + vol.Schema({ + vol.Optional(CONF_SERVER_ID): cv.positive_int, + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): + vol.All(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_MANUAL, default=False): cv.boolean, + vol.Optional( + CONF_MONITORED_CONDITIONS, + default=list(SENSOR_TYPES) + ): vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]) + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=DEFAULT_INTERVAL + ) + ) }, extra=vol.ALLOW_EXTRA) @@ -44,7 +55,7 @@ async def async_setup(hass, config): if not conf[CONF_MANUAL]: async_track_time_interval( - hass, data.update, conf[CONF_UPDATE_INTERVAL] + hass, data.update, conf[CONF_SCAN_INTERVAL] ) def update(call=None): diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 1a6f35fe8d8..397e21922d9 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -6,7 +6,8 @@ import logging import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_UPDATE_INTERVAL +from homeassistant.const import CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL, \ + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later @@ -23,17 +24,23 @@ REQUIREMENTS = ['tellduslive==0.10.10'] _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All( vol.Schema({ vol.Optional(CONF_HOST, default=DOMAIN): cv.string, - vol.Optional(CONF_UPDATE_INTERVAL, default=SCAN_INTERVAL): - (vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))) + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)), + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)), }), - }, - extra=vol.ALLOW_EXTRA, -) + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=SCAN_INTERVAL + ) + ) +}, extra=vol.ALLOW_EXTRA) DATA_CONFIG_ENTRY_LOCK = 'tellduslive_config_entry_lock' CONFIG_ENTRY_IS_SETUP = 'telldus_config_entry_is_setup' @@ -102,7 +109,7 @@ async def async_setup(hass, config): context={'source': config_entries.SOURCE_IMPORT}, data={ KEY_HOST: config[DOMAIN].get(CONF_HOST), - KEY_SCAN_INTERVAL: config[DOMAIN].get(CONF_UPDATE_INTERVAL), + KEY_SCAN_INTERVAL: config[DOMAIN][CONF_SCAN_INTERVAL], })) return True diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index 9dbaadf9bee..7e72607c2f3 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -6,7 +6,8 @@ import voluptuous as vol from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_RESOURCES, - CONF_UPDATE_INTERVAL) + CONF_UPDATE_INTERVAL, CONF_SCAN_INTERVAL, + CONF_UPDATE_INTERVAL_INVALIDATION_VERSION) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -83,20 +84,30 @@ RESOURCES = [ ] CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): ( - vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL))), - vol.Optional(CONF_NAME, default={}): - cv.schema_with_slug_keys(cv.string), - vol.Optional(CONF_RESOURCES): vol.All( - cv.ensure_list, [vol.In(RESOURCES)]), - vol.Optional(CONF_REGION): cv.string, - vol.Optional(CONF_SERVICE_URL): cv.string, - vol.Optional(CONF_MUTABLE, default=True): cv.boolean, - vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean, - }), + DOMAIN: vol.All( + vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL): + vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): + vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)), + vol.Optional(CONF_NAME, default={}): + cv.schema_with_slug_keys(cv.string), + vol.Optional(CONF_RESOURCES): vol.All( + cv.ensure_list, [vol.In(RESOURCES)]), + vol.Optional(CONF_REGION): cv.string, + vol.Optional(CONF_SERVICE_URL): cv.string, + vol.Optional(CONF_MUTABLE, default=True): cv.boolean, + vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean, + }), + cv.deprecated( + CONF_UPDATE_INTERVAL, + replacement_key=CONF_SCAN_INTERVAL, + invalidation_version=CONF_UPDATE_INTERVAL_INVALIDATION_VERSION, + default=DEFAULT_UPDATE_INTERVAL + ) + ) }, extra=vol.ALLOW_EXTRA) @@ -112,7 +123,7 @@ async def async_setup(hass, config): service_url=config[DOMAIN].get(CONF_SERVICE_URL), region=config[DOMAIN].get(CONF_REGION)) - interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL) + interval = config[DOMAIN][CONF_SCAN_INTERVAL] data = hass.data[DATA_KEY] = VolvoData(config) diff --git a/homeassistant/const.py b/homeassistant/const.py index 56e174effdf..54901feb73b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -147,7 +147,11 @@ CONF_TTL = 'ttl' CONF_TYPE = 'type' CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' CONF_UNIT_SYSTEM = 'unit_system' + +# Deprecated in 0.88.0, invalidated in 0.91.0, remove in 0.92.0 CONF_UPDATE_INTERVAL = 'update_interval' +CONF_UPDATE_INTERVAL_INVALIDATION_VERSION = '0.91.0' + CONF_URL = 'url' CONF_USERNAME = 'username' CONF_VALUE_TEMPLATE = 'value_template' diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 3b01a01fc96..ab385019b10 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -606,7 +606,8 @@ def deprecated(key: str, else: value = default if (replacement_key - and replacement_key not in config + and (replacement_key not in config + or default == config.get(replacement_key)) and value is not None): config[replacement_key] = value diff --git a/tests/components/freedns/test_init.py b/tests/components/freedns/test_init.py index 784926912cd..1996b02d8d0 100644 --- a/tests/components/freedns/test_init.py +++ b/tests/components/freedns/test_init.py @@ -24,7 +24,7 @@ def setup_freedns(hass, aioclient_mock): hass.loop.run_until_complete(async_setup_component(hass, freedns.DOMAIN, { freedns.DOMAIN: { 'access_token': ACCESS_TOKEN, - 'update_interval': UPDATE_INTERVAL, + 'scan_interval': UPDATE_INTERVAL, } })) @@ -62,7 +62,7 @@ def test_setup_fails_if_wrong_token(hass, aioclient_mock): result = yield from async_setup_component(hass, freedns.DOMAIN, { freedns.DOMAIN: { 'access_token': ACCESS_TOKEN, - 'update_interval': UPDATE_INTERVAL, + 'scan_interval': UPDATE_INTERVAL, } }) assert not result diff --git a/tests/components/sensor/test_darksky.py b/tests/components/sensor/test_darksky.py index 33a13f013de..58ce932020a 100644 --- a/tests/components/sensor/test_darksky.py +++ b/tests/components/sensor/test_darksky.py @@ -21,7 +21,7 @@ VALID_CONFIG_MINIMAL = { 'api_key': 'foo', 'forecast': [1, 2], 'monitored_conditions': ['summary', 'icon', 'temperature_high'], - 'update_interval': timedelta(seconds=120), + 'scan_interval': timedelta(seconds=120), } } @@ -31,7 +31,7 @@ INVALID_CONFIG_MINIMAL = { 'api_key': 'foo', 'forecast': [1, 2], 'monitored_conditions': ['sumary', 'iocn', 'temperature_high'], - 'update_interval': timedelta(seconds=120), + 'scan_interval': timedelta(seconds=120), } } @@ -45,7 +45,7 @@ VALID_CONFIG_LANG_DE = { 'monitored_conditions': ['summary', 'icon', 'temperature_high', 'minutely_summary', 'hourly_summary', 'daily_summary', 'humidity', ], - 'update_interval': timedelta(seconds=120), + 'scan_interval': timedelta(seconds=120), } } @@ -56,7 +56,7 @@ INVALID_CONFIG_LANG = { 'forecast': [1, 2], 'language': 'yz', 'monitored_conditions': ['summary', 'icon', 'temperature_high'], - 'update_interval': timedelta(seconds=120), + 'scan_interval': timedelta(seconds=120), } } @@ -138,8 +138,11 @@ class TestDarkSkySetup(unittest.TestCase): msg = '400 Client Error: Bad Request for url: {}'.format(url) mock_get_forecast.side_effect = HTTPError(msg,) - response = darksky.setup_platform(self.hass, VALID_CONFIG_MINIMAL, - MagicMock()) + response = darksky.setup_platform( + self.hass, + VALID_CONFIG_MINIMAL['sensor'], + MagicMock() + ) assert not response @requests_mock.Mocker() diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index cefde564035..d83d32c88e3 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -713,7 +713,7 @@ def test_deprecated_with_default(caplog, schema): def test_deprecated_with_replacement_key_and_default(caplog, schema): """ - Test deprecation behaves correctly when only a replacement key is provided. + Test deprecation with a replacement key and default. Expected behavior: - Outputs the appropriate deprecation warning if key is detected @@ -748,6 +748,22 @@ def test_deprecated_with_replacement_key_and_default(caplog, schema): assert len(caplog.records) == 0 assert {'venus': True, 'jupiter': False} == output + deprecated_schema_with_default = vol.All( + vol.Schema({ + 'venus': cv.boolean, + vol.Optional('mars', default=False): cv.boolean, + vol.Optional('jupiter', default=False): cv.boolean + }), + cv.deprecated('mars', replacement_key='jupiter', default=False) + ) + + test_data = {'mars': True} + output = deprecated_schema_with_default(test_data.copy()) + assert len(caplog.records) == 1 + assert ("The 'mars' option (with value 'True') is deprecated, " + "please replace it with 'jupiter'") in caplog.text + assert {'jupiter': True} == output + def test_deprecated_with_replacement_key_invalidation_version_default( caplog, schema, version From 8c5763624c92216b99eacf8f42fb0081311b60ec Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Sun, 17 Feb 2019 06:24:13 +0100 Subject: [PATCH 041/253] Add Groups to Homematic IP (#21076) * Added HmIP-Groups * Fix imports * Removed config options from conflig_flow * fix tests * Removed config options * reverted smaller code changes * changes after review * minor fix * Fixed comments --- .../homematicip_cloud/alarm_control_panel.py | 5 +- .../homematicip_cloud/binary_sensor.py | 105 ++++++++++++++++++ .../components/homematicip_cloud/switch.py | 29 +++++ 3 files changed, 137 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index dc249775e3d..efa1ea1f46e 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -42,7 +42,7 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel): def __init__(self, home, device): """Initialize the security zone group.""" device.modelType = 'Group-SecurityZone' - device.windowState = '' + device.windowState = None super().__init__(home, device) @property @@ -52,7 +52,8 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel): if self._device.active: if (self._device.sabotage or self._device.motionDetected or - self._device.windowState == WindowState.OPEN): + self._device.windowState == WindowState.OPEN or + self._device.windowState == WindowState.TILTED): return STATE_ALARM_TRIGGERED active = self._home.get_security_zones_activation() diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 9ed9d29ad39..4b82a500bde 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -9,6 +9,14 @@ DEPENDENCIES = ['homematicip_cloud'] _LOGGER = logging.getLogger(__name__) +ATTR_MOTIONDETECTED = 'motion detected' +ATTR_PRESENCEDETECTED = 'presence detected' +ATTR_POWERMAINSFAILURE = 'power mains failure' +ATTR_WINDOWSTATE = 'window state' +ATTR_MOISTUREDETECTED = 'moisture detected' +ATTR_WATERLEVELDETECTED = 'water level detected' +ATTR_SMOKEDETECTORALARM = 'smoke detector alarm' + async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): @@ -23,6 +31,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): AsyncWaterSensor, AsyncRotaryHandleSensor, AsyncMotionDetectorPushButton) + from homematicip.group import ( + SecurityGroup, SecurityZoneGroup) + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: @@ -36,6 +47,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): elif isinstance(device, AsyncWaterSensor): devices.append(HomematicipWaterDetector(home, device)) + for group in home.groups: + if isinstance(group, SecurityGroup): + devices.append(HomematicipSecuritySensorGroup(home, group)) + elif isinstance(group, SecurityZoneGroup): + devices.append(HomematicipSecurityZoneSensorGroup(home, group)) + if devices: async_add_entities(devices) @@ -104,3 +121,91 @@ class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): def is_on(self): """Return true if moisture or waterlevel is detected.""" return self._device.moistureDetected or self._device.waterlevelDetected + + +class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, + BinarySensorDevice): + """Representation of a HomematicIP Cloud security zone group.""" + + def __init__(self, home, device, post='SecurityZone'): + """Initialize security zone group.""" + device.modelType = 'HmIP-{}'.format(post) + super().__init__(home, device, post) + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'safety' + + @property + def device_state_attributes(self): + """Return the state attributes of the security zone group.""" + attr = super().device_state_attributes + + if self._device.motionDetected: + attr.update({ATTR_MOTIONDETECTED: True}) + if self._device.presenceDetected: + attr.update({ATTR_PRESENCEDETECTED: True}) + from homematicip.base.enums import WindowState + if self._device.windowState is not None and \ + self._device.windowState != WindowState.CLOSED: + attr.update({ATTR_WINDOWSTATE: str(self._device.windowState)}) + + return attr + + @property + def is_on(self): + """Return true if security issue detected.""" + if self._device.motionDetected or \ + self._device.presenceDetected: + return True + from homematicip.base.enums import WindowState + if self._device.windowState is not None and \ + self._device.windowState != WindowState.CLOSED: + return True + return False + + +class HomematicipSecuritySensorGroup(HomematicipSecurityZoneSensorGroup, + BinarySensorDevice): + """Representation of a HomematicIP security group.""" + + def __init__(self, home, device): + """Initialize security group.""" + super().__init__(home, device, 'Sensors') + + @property + def device_state_attributes(self): + """Return the state attributes of the security group.""" + attr = super().device_state_attributes + + if self._device.powerMainsFailure: + attr.update({ATTR_POWERMAINSFAILURE: True}) + if self._device.moistureDetected: + attr.update({ATTR_MOISTUREDETECTED: True}) + if self._device.waterlevelDetected: + attr.update({ATTR_WATERLEVELDETECTED: True}) + from homematicip.base.enums import SmokeDetectorAlarmType + if self._device.smokeDetectorAlarmType is not None and \ + self._device.smokeDetectorAlarmType != \ + SmokeDetectorAlarmType.IDLE_OFF: + attr.update({ATTR_SMOKEDETECTORALARM: str( + self._device.smokeDetectorAlarmType)}) + + return attr + + @property + def is_on(self): + """Return true if security issue detected.""" + parent_is_on = super().is_on + from homematicip.base.enums import SmokeDetectorAlarmType + if parent_is_on or \ + self._device.powerMainsFailure or \ + self._device.moistureDetected or \ + self._device.waterlevelDetected: + return True + if self._device.smokeDetectorAlarmType is not None and \ + self._device.smokeDetectorAlarmType != \ + SmokeDetectorAlarmType.IDLE_OFF: + return True + return False diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index ad378074621..057673d8f9b 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -29,6 +29,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): FullFlushSwitchMeasuring, ) + from homematicip.group import SwitchingGroup + home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: @@ -43,6 +45,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): elif isinstance(device, PlugableSwitch): devices.append(HomematicipSwitch(home, device)) + for group in home.groups: + if isinstance(group, SwitchingGroup): + devices.append( + HomematicipGroupSwitch(home, group)) + if devices: async_add_entities(devices) @@ -68,6 +75,28 @@ class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): await self._device.turn_off() +class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): + """representation of a HomematicIP switching group.""" + + def __init__(self, home, device, post='Group'): + """Initialize switching group.""" + device.modelType = 'HmIP-{}'.format(post) + super().__init__(home, device, post) + + @property + def is_on(self): + """Return true if group is on.""" + return self._device.on + + async def async_turn_on(self, **kwargs): + """Turn the group on.""" + await self._device.turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the group off.""" + await self._device.turn_off() + + class HomematicipSwitchMeasuring(HomematicipSwitch): """Representation of a HomematicIP measuring switch device.""" From 9125e49628ddc57de08eb67bedc9e134c8d7a2ac Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 17 Feb 2019 08:04:29 +0100 Subject: [PATCH 042/253] Fix battery_level error - HomeKit (#21120) --- homeassistant/components/homekit/accessories.py | 2 ++ tests/components/homekit/test_accessories.py | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 4adeda2465a..2738fafbfdb 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -122,6 +122,8 @@ class HomeAccessory(Accessory): """ battery_level = convert_to_float( new_state.attributes.get(ATTR_BATTERY_LEVEL)) + if battery_level is None: + return 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, diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 15ab6d7413e..6f3957827eb 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -100,7 +100,7 @@ async def test_home_accessory(hass, hk_driver): assert serv.get_characteristic(CHAR_MODEL).value == 'Test Model' -async def test_battery_service(hass, hk_driver): +async def test_battery_service(hass, hk_driver, caplog): """Test battery service.""" entity_id = 'homekit.accessory' hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 50}) @@ -124,6 +124,13 @@ async def test_battery_service(hass, hk_driver): assert acc._char_low_battery.value == 1 assert acc._char_charging.value == 2 + hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 'error'}) + 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 + assert 'ERROR' not in caplog.text + # Test charging hass.states.async_set(entity_id, None, { ATTR_BATTERY_LEVEL: 10, ATTR_BATTERY_CHARGING: True}) From 2155a861cdede6604fb72d5acc8c7a67658702af Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sat, 16 Feb 2019 23:06:42 -0800 Subject: [PATCH 043/253] Remove outdated url pattern match support for static file hosting (#21109) --- homeassistant/components/http/__init__.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index cdbbbe551ab..7dca332058c 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -277,19 +277,7 @@ class HomeAssistantHTTP: """Serve file from disk.""" return web.FileResponse(path) - # aiohttp supports regex matching for variables. Using that as temp - # to work around cache busting MD5. - # Turns something like /static/dev-panel.html into - # /static/{filename:dev-panel(-[a-z0-9]{32}|)\.html} - base, ext = os.path.splitext(url_path) - if ext: - base, file = base.rsplit('/', 1) - regex = r"{}(-[a-z0-9]{{32}}|){}".format(file, ext) - url_pattern = "{}/{{filename:{}}}".format(base, regex) - else: - url_pattern = url_path - - self.app.router.add_route('GET', url_pattern, serve_file) + self.app.router.add_route('GET', url_path, serve_file) async def start(self): """Start the aiohttp server.""" From 816364bfd925d0b0be3ddf09ec6593d8ac272edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 17 Feb 2019 10:55:33 +0200 Subject: [PATCH 044/253] Upgrade pytest to 4.2.1 (#21112) * Upgrade pytest to 4.2.0 * Upgrade pytest to 4.2.1 * Make litejet switch test work with pytest 4.2 --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switch/test_litejet.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index b9da9890c61..8e647010426 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.6.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==4.1.1 +pytest==4.2.1 requests_mock==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40e7a37326d..6525bbda40d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,7 +14,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.6.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==4.1.1 +pytest==4.2.1 requests_mock==1.5.2 diff --git a/tests/components/switch/test_litejet.py b/tests/components/switch/test_litejet.py index f1d23f48b86..a35b6f760f3 100644 --- a/tests/components/switch/test_litejet.py +++ b/tests/components/switch/test_litejet.py @@ -53,9 +53,9 @@ class TestLiteJetSwitch(unittest.TestCase): 'port': '/tmp/this_will_be_mocked', } } - if method == self.test_include_switches_False: + if method == self.__class__.test_include_switches_False: config['litejet']['include_switches'] = False - elif method != self.test_include_switches_unspecified: + elif method != self.__class__.test_include_switches_unspecified: config['litejet']['include_switches'] = True assert setup.setup_component(self.hass, litejet.DOMAIN, config) From df598544d6f5a72e7c951872cc5930d01d60ea62 Mon Sep 17 00:00:00 2001 From: msvinth Date: Sun, 17 Feb 2019 11:45:58 +0100 Subject: [PATCH 045/253] Make Netatmo able to discover both Weather station and Health Coach (#20274) * Resolve conflicts and review comments * Corretly handle HomeCoach in manual setup * Fix code reivew comments * Move import back int methods * Formatting fix * Lint fix --- homeassistant/components/netatmo/sensor.py | 94 +++++++++++++++++----- 1 file changed, 72 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 78a118528b9..3f04f22ac76 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -47,6 +47,7 @@ SENSOR_TYPES = { 'rf_status_lvl': ['Radio_lvl', '', 'mdi:signal', None], 'wifi_status': ['Wifi', '', 'mdi:wifi', None], 'wifi_status_lvl': ['Wifi_lvl', 'dBm', 'mdi:wifi', None], + 'health_idx': ['Health', '', 'mdi:cloud', None], } MODULE_SCHEMA = vol.Schema({ @@ -67,23 +68,55 @@ MODULE_TYPE_INDOOR = 'NAModule4' def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available Netatmo weather sensors.""" netatmo = hass.components.netatmo - data = NetAtmoData(netatmo.NETATMO_AUTH, config.get(CONF_STATION, None)) dev = [] + if CONF_MODULES in config: + manual_config(netatmo, config, dev) + else: + auto_config(netatmo, config, dev) + + if dev: + add_entities(dev, True) + + +def manual_config(netatmo, config, dev): + """Handle manual configuration.""" import pyatmo - try: - if CONF_MODULES in config: + + all_classes = all_product_classes() + not_handled = {} + for data_class in all_classes: + data = NetAtmoData(netatmo.NETATMO_AUTH, data_class, + config.get(CONF_STATION)) + try: # Iterate each module for module_name, monitored_conditions in \ config[CONF_MODULES].items(): # Test if module exists if module_name not in data.get_module_names(): - _LOGGER.error('Module name: "%s" not found', module_name) - continue - # Only create sensors for monitored properties - for variable in monitored_conditions: - dev.append(NetAtmoSensor(data, module_name, variable)) - else: + not_handled[module_name] = \ + not_handled[module_name]+1 \ + if module_name in not_handled else 1 + else: + # Only create sensors for monitored properties + for variable in monitored_conditions: + dev.append(NetAtmoSensor(data, module_name, variable)) + except pyatmo.NoDevice: + continue + + for module_name, count in not_handled.items(): + if count == len(all_classes): + _LOGGER.error('Module name: "%s" not found', module_name) + + +def auto_config(netatmo, config, dev): + """Handle auto configuration.""" + import pyatmo + + for data_class in all_product_classes(): + data = NetAtmoData(netatmo.NETATMO_AUTH, data_class, + config.get(CONF_STATION)) + try: for module_name in data.get_module_names(): for variable in \ data.station_data.monitoredConditions(module_name): @@ -92,10 +125,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: _LOGGER.warning("Ignoring unknown var %s for mod %s", variable, module_name) - except pyatmo.NoDevice: - return None + except pyatmo.NoDevice: + continue - add_entities(dev, True) + +def all_product_classes(): + """Provide all handled Netatmo product classes.""" + import pyatmo + + return [pyatmo.WeatherStationData, pyatmo.HomeCoachData] class NetAtmoSensor(Entity): @@ -299,6 +337,17 @@ class NetAtmoSensor(Entity): self._state = "High" elif data['wifi_status'] <= 55: self._state = "Full" + elif self.type == 'health_idx': + if data['health_idx'] == 0: + self._state = "Healthy" + elif data['health_idx'] == 1: + self._state = "Fine" + elif data['health_idx'] == 2: + self._state = "Fair" + elif data['health_idx'] == 3: + self._state = "Poor" + elif data['health_idx'] == 4: + self._state = "Unhealthy" except KeyError: _LOGGER.error("No %s data found for %s", self.type, self.module_name) @@ -309,9 +358,10 @@ class NetAtmoSensor(Entity): class NetAtmoData: """Get the latest data from NetAtmo.""" - def __init__(self, auth, station): + def __init__(self, auth, data_class, station): """Initialize the data object.""" self.auth = auth + self.data_class = data_class self.data = None self.station_data = None self.station = station @@ -321,6 +371,8 @@ class NetAtmoData: def get_module_names(self): """Return all module available on the API as a list.""" self.update() + if not self.data: + return [] return self.data.keys() def _detect_platform_type(self): @@ -328,14 +380,12 @@ class NetAtmoData: The return can be a WeatherStationData or a HomeCoachData. """ - import pyatmo - for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: - try: - station_data = data_class(self.auth) - _LOGGER.debug("%s detected!", str(data_class.__name__)) - return station_data - except TypeError: - continue + try: + station_data = self.data_class(self.auth) + _LOGGER.debug("%s detected!", str(self.data_class.__name__)) + return station_data + except TypeError: + return def update(self): """Call the Netatmo API to update the data. @@ -366,7 +416,7 @@ class NetAtmoData: newinterval = self.data[module]['When'] break except TypeError: - _LOGGER.error("No modules found!") + _LOGGER.debug("No %s modules found", self.data_class.__name__) if newinterval: # Try and estimate when fresh data will be available From 33b8dbe73afc59cee77ead7409271fd7ef1e9151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9-Marc=20Simard?= Date: Sun, 17 Feb 2019 05:46:08 -0500 Subject: [PATCH 046/253] Return None if no GTFS departures found (#20919) --- homeassistant/components/sensor/gtfs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/gtfs.py b/homeassistant/components/sensor/gtfs.py index 081361aa32e..94f21287e39 100644 --- a/homeassistant/components/sensor/gtfs.py +++ b/homeassistant/components/sensor/gtfs.py @@ -205,7 +205,7 @@ class GTFSDepartureSensor(Entity): self._icon = ICON self._name = '' self._unit_of_measurement = 'min' - self._state = 0 + self._state = None self._attributes = {} self.lock = threading.Lock() self.update() @@ -241,7 +241,7 @@ class GTFSDepartureSensor(Entity): self._departure = get_next_departure( self._pygtfs, self.origin, self.destination, self._offset) if not self._departure: - self._state = 0 + self._state = None self._attributes = {'Info': 'No more departures today'} if self._name == '': self._name = (self._custom_name or DEFAULT_NAME) From 847711ddc9afaf7d82af320eedfc4397f4a9cfd2 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sun, 17 Feb 2019 12:31:47 +0100 Subject: [PATCH 047/253] Add webhook support for Netatmo Cameras (#20755) Add webhook support for Netatmo Cameras --- homeassistant/components/netatmo/__init__.py | 134 +++++++++++++++++- homeassistant/components/netatmo/camera.py | 1 + .../components/netatmo/services.yaml | 8 ++ 3 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/netatmo/services.yaml diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 495e22aae24..c496553f057 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,26 +1,64 @@ """Support for the Netatmo devices.""" import logging +import json from datetime import timedelta from urllib.error import HTTPError import voluptuous as vol from homeassistant.const import ( - CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, CONF_DISCOVERY) + CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, CONF_DISCOVERY, CONF_URL, + EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle REQUIREMENTS = ['pyatmo==1.8'] +DEPENDENCIES = ['webhook'] _LOGGER = logging.getLogger(__name__) CONF_SECRET_KEY = 'secret_key' +CONF_WEBHOOKS = 'webhooks' DOMAIN = 'netatmo' +SERVICE_ADDWEBHOOK = 'addwebhook' +SERVICE_DROPWEBHOOK = 'dropwebhook' + NETATMO_AUTH = None +NETATMO_WEBHOOK_URL = None +NETATMO_PERSONS = {} + +DEFAULT_PERSON = 'Unknown' DEFAULT_DISCOVERY = True +DEFAULT_WEBHOOKS = False + +EVENT_PERSON = 'person' +EVENT_MOVEMENT = 'movement' +EVENT_HUMAN = 'human' +EVENT_ANIMAL = 'animal' +EVENT_VEHICLE = 'vehicle' + +EVENT_BUS_PERSON = 'netatmo_person' +EVENT_BUS_MOVEMENT = 'netatmo_movement' +EVENT_BUS_HUMAN = 'netatmo_human' +EVENT_BUS_ANIMAL = 'netatmo_animal' +EVENT_BUS_VEHICLE = 'netatmo_vehicle' +EVENT_BUS_OTHER = 'netatmo_other' + +ATTR_ID = 'id' +ATTR_PSEUDO = 'pseudo' +ATTR_NAME = 'name' +ATTR_EVENT_TYPE = 'event_type' +ATTR_MESSAGE = 'message' +ATTR_CAMERA_ID = 'camera_id' +ATTR_HOME_NAME = 'home_name' +ATTR_PERSONS = 'persons' +ATTR_IS_KNOWN = 'is_known' +ATTR_FACE_URL = 'face_url' +ATTR_SNAPSHOT_URL = 'snapshot_url' +ATTR_VIGNETTE_URL = 'vignette_url' MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=10) @@ -31,16 +69,23 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_SECRET_KEY): cv.string, vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_WEBHOOKS, default=DEFAULT_WEBHOOKS): cv.boolean, vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, }) }, extra=vol.ALLOW_EXTRA) +SCHEMA_SERVICE_ADDWEBHOOK = vol.Schema({ + vol.Optional(CONF_URL): cv.string, +}) + +SCHEMA_SERVICE_DROPWEBHOOK = vol.Schema({}) + def setup(hass, config): """Set up the Netatmo devices.""" import pyatmo - global NETATMO_AUTH + global NETATMO_AUTH, NETATMO_WEBHOOK_URL try: NETATMO_AUTH = pyatmo.ClientAuth( config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY], @@ -56,9 +101,88 @@ def setup(hass, config): for component in 'camera', 'sensor', 'binary_sensor', 'climate': discovery.load_platform(hass, component, DOMAIN, {}, config) + if config[DOMAIN][CONF_WEBHOOKS]: + webhook_id = hass.components.webhook.async_generate_id() + NETATMO_WEBHOOK_URL = hass.components.webhook.async_generate_url( + webhook_id) + hass.components.webhook.async_register( + DOMAIN, 'Netatmo', webhook_id, handle_webhook) + NETATMO_AUTH.addwebhook(NETATMO_WEBHOOK_URL) + hass.bus.listen_once( + EVENT_HOMEASSISTANT_STOP, dropwebhook) + + def _service_addwebhook(service): + """Service to (re)add webhooks during runtime.""" + url = service.data.get(CONF_URL) + if url is None: + url = NETATMO_WEBHOOK_URL + _LOGGER.info("Adding webhook for URL: %s", url) + NETATMO_AUTH.addwebhook(url) + + hass.services.register( + DOMAIN, SERVICE_ADDWEBHOOK, _service_addwebhook, + schema=SCHEMA_SERVICE_ADDWEBHOOK) + + def _service_dropwebhook(service): + """Service to drop webhooks during runtime.""" + _LOGGER.info("Dropping webhook") + NETATMO_AUTH.dropwebhook() + + hass.services.register( + DOMAIN, SERVICE_DROPWEBHOOK, _service_dropwebhook, + schema=SCHEMA_SERVICE_DROPWEBHOOK) + return True +def dropwebhook(hass): + """Drop the webhook subscription.""" + NETATMO_AUTH.dropwebhook() + + +async def handle_webhook(hass, webhook_id, request): + """Handle webhook callback.""" + body = await request.text() + try: + data = json.loads(body) if body else {} + except ValueError: + return None + + _LOGGER.debug("Got webhook data: %s", data) + published_data = { + ATTR_EVENT_TYPE: data.get(ATTR_EVENT_TYPE), + ATTR_HOME_NAME: data.get(ATTR_HOME_NAME), + ATTR_CAMERA_ID: data.get(ATTR_CAMERA_ID), + ATTR_MESSAGE: data.get(ATTR_MESSAGE) + } + if data.get(ATTR_EVENT_TYPE) == EVENT_PERSON: + for person in data[ATTR_PERSONS]: + published_data[ATTR_ID] = person.get(ATTR_ID) + published_data[ATTR_NAME] = NETATMO_PERSONS.get( + published_data[ATTR_ID], DEFAULT_PERSON) + published_data[ATTR_IS_KNOWN] = person.get(ATTR_IS_KNOWN) + published_data[ATTR_FACE_URL] = person.get(ATTR_FACE_URL) + hass.bus.async_fire(EVENT_BUS_PERSON, published_data) + elif data.get(ATTR_EVENT_TYPE) == EVENT_MOVEMENT: + published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) + published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) + hass.bus.async_fire(EVENT_BUS_MOVEMENT, published_data) + elif data.get(ATTR_EVENT_TYPE) == EVENT_HUMAN: + published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) + published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) + hass.bus.async_fire(EVENT_BUS_HUMAN, published_data) + elif data.get(ATTR_EVENT_TYPE) == EVENT_ANIMAL: + published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) + published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) + hass.bus.async_fire(EVENT_BUS_ANIMAL, published_data) + elif data.get(ATTR_EVENT_TYPE) == EVENT_VEHICLE: + hass.bus.async_fire(EVENT_BUS_VEHICLE, published_data) + published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) + published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) + else: + hass.bus.async_fire(EVENT_BUS_OTHER, data) + + class CameraData: """Get the latest data from Netatmo.""" @@ -101,6 +225,12 @@ class CameraData: home=home, cid=cid) return self.camera_type + def get_persons(self): + """Gather person data for webhooks.""" + global NETATMO_PERSONS + for person_id, person_data in self.camera_data.persons.items(): + NETATMO_PERSONS[person_id] = person_data.get(ATTR_PSEUDO) + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the Netatmo API to update the data.""" diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index a3a5461631d..af56dc6e621 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -40,6 +40,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): continue add_entities([NetatmoCamera(data, camera_name, home, camera_type, verify_ssl)]) + data.get_persons() except pyatmo.NoDevice: return None diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml new file mode 100644 index 00000000000..7bb990caf97 --- /dev/null +++ b/homeassistant/components/netatmo/services.yaml @@ -0,0 +1,8 @@ +addwebhook: + description: Add webhook during runtime (e.g. if it has been banned). + fields: + url: + description: URL for which to add the webhook. + example: https://yourdomain.com:443/api/webhook/webhook_id +dropwebhook: + description: Drop active webhooks. \ No newline at end of file From 92518602016c05109e3b6a3696f0988d5a33cd49 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 17 Feb 2019 16:58:46 +0100 Subject: [PATCH 048/253] Logging to find what deCONZ events get created (#20551) * Helpful logging to easily find what events get created --- homeassistant/components/deconz/gateway.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index fe9fc4b7752..2b53a0998d0 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -8,7 +8,7 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.util import slugify from .const import ( - DECONZ_REACHABLE, CONF_ALLOW_CLIP_SENSOR, SUPPORTED_PLATFORMS) + _LOGGER, DECONZ_REACHABLE, CONF_ALLOW_CLIP_SENSOR, SUPPORTED_PLATFORMS) class DeconzGateway: @@ -140,6 +140,7 @@ class DeconzEvent: self._device.register_async_callback(self.async_update_callback) self._event = 'deconz_{}'.format(CONF_EVENT) self._id = slugify(self._device.name) + _LOGGER.debug("deCONZ event created: %s", self._id) @callback def async_will_remove_from_hass(self) -> None: From 4e7cfc923d9359217c56bce7558c5983d625826b Mon Sep 17 00:00:00 2001 From: lapy Date: Sun, 17 Feb 2019 16:55:06 +0000 Subject: [PATCH 049/253] Add traccar scan_interval configuration option (#21079) * Added scan_interval configuration option * Fixed trailing whitespace and indentation * Update traccar.py * Update homeassistant/components/device_tracker/traccar.py Co-Authored-By: lapy --- homeassistant/components/device_tracker/traccar.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/device_tracker/traccar.py b/homeassistant/components/device_tracker/traccar.py index b288b8633d1..4d340100849 100644 --- a/homeassistant/components/device_tracker/traccar.py +++ b/homeassistant/components/device_tracker/traccar.py @@ -12,7 +12,8 @@ import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, - CONF_PASSWORD, CONF_USERNAME, ATTR_BATTERY_LEVEL) + CONF_PASSWORD, CONF_USERNAME, ATTR_BATTERY_LEVEL, + CONF_SCAN_INTERVAL) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_time_interval @@ -31,6 +32,7 @@ ATTR_SPEED = 'speed' ATTR_TRACKER = 'tracker' DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PASSWORD): cv.string, @@ -50,15 +52,17 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): api = API(hass.loop, session, config[CONF_USERNAME], config[CONF_PASSWORD], config[CONF_HOST], config[CONF_PORT], config[CONF_SSL]) - scanner = TraccarScanner(api, hass, async_see) + scanner = TraccarScanner( + api, hass, async_see, config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)) return await scanner.async_init() class TraccarScanner: """Define an object to retrieve Traccar data.""" - def __init__(self, api, hass, async_see): + def __init__(self, api, hass, async_see, scan_interval): """Initialize.""" + self._scan_interval = scan_interval self._async_see = async_see self._api = api self._hass = hass @@ -70,7 +74,7 @@ class TraccarScanner: await self._async_update() async_track_time_interval(self._hass, self._async_update, - DEFAULT_SCAN_INTERVAL) + self._scan_interval) return self._api.authenticated From 72ef9670e6fd18192330c9ffcba06f53f746c315 Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Sun, 17 Feb 2019 13:41:55 -0700 Subject: [PATCH 050/253] Add component media player.ps4 (#21074) * Added PS4/ __init__.py * Create en.json * Create config_flow.py * Create const.py * Create media_player.py * Create services.yaml * Create strings.json * Create __init__.py * Add test_config_flow.py/ Finished adding PS4 files * Rewrote for loop into short-hand * bumped pyps4 to 0.2.8 * Pass in helper() * Rewrite func * Fixed test * Added import in init * bump to 0.2.9 * bump to 0.3.0 * Removed line * lint * Add ps4 to flows list * Added pyps4-homeassistant with script * Added pyps4 * Added pypys4 to test * removed list def * reformatted service call dicts * removed config from device class * typo * removed line * reformatted .. format * redefined property * reformat load games func * Add __init__ and media_player.py to coveragerc * Fix for test * remove init * remove blank line * remove mock_coro * Revert "remove init" This reverts commit b68996aa34699bf38781e153acdd597579e8131f. * Correct permissions * fixes * fixes --- .coveragerc | 2 + .../components/ps4/.translations/en.json | 32 ++ homeassistant/components/ps4/__init__.py | 33 ++ homeassistant/components/ps4/config_flow.py | 123 ++++++ homeassistant/components/ps4/const.py | 5 + homeassistant/components/ps4/media_player.py | 372 ++++++++++++++++++ homeassistant/components/ps4/services.yaml | 9 + homeassistant/components/ps4/strings.json | 32 ++ homeassistant/config_entries.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/ps4/__init__.py | 1 + tests/components/ps4/test_config_flow.py | 149 +++++++ 14 files changed, 766 insertions(+) create mode 100644 homeassistant/components/ps4/.translations/en.json create mode 100644 homeassistant/components/ps4/__init__.py create mode 100644 homeassistant/components/ps4/config_flow.py create mode 100644 homeassistant/components/ps4/const.py create mode 100644 homeassistant/components/ps4/media_player.py create mode 100644 homeassistant/components/ps4/services.yaml create mode 100644 homeassistant/components/ps4/strings.json create mode 100644 tests/components/ps4/__init__.py create mode 100644 tests/components/ps4/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 8b51a34df61..281fcc3ec19 100644 --- a/.coveragerc +++ b/.coveragerc @@ -381,6 +381,8 @@ omit = homeassistant/components/plum_lightpad/* homeassistant/components/point/* homeassistant/components/prometheus/* + homeassistant/components/ps4/__init__.py + homeassistant/components/ps4/media_player.py homeassistant/components/qwikswitch/* homeassistant/components/rachio/* homeassistant/components/rainbird/* diff --git a/homeassistant/components/ps4/.translations/en.json b/homeassistant/components/ps4/.translations/en.json new file mode 100644 index 00000000000..b546280a7ce --- /dev/null +++ b/homeassistant/components/ps4/.translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "title": "PlayStation 4", + "step": { + "creds": { + "title": "PlayStation 4", + "description": "Credentials needed. Press 'Submit' and then in the PS4 2nd Screen App, refresh devices and select the 'Home-Assistant' device to continue." + }, + "link": { + "title": "PlayStation 4", + "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed.", + "data": { + "region": "Region", + "name": "Name", + "code": "PIN", + "ip_address": "IP Address" + } + } + }, + "error": { + "not_ready": "PlayStation 4 is not on or connected to network.", + "login_failed": "Failed to pair to PlayStation 4. Verify PIN is correct." + }, + "abort": { + "credential_error": "Error fetching credentials.", + "no_devices_found": "No PlayStation 4 devices found on the network.", + "devices_configured": "All devices found are already configured.", + "port_987_bind_error": "Could not bind to UDP port 987. Port in use or additional configuration required. See component documentation at: https://home-assistant.io/components/media_player.ps4/", + "port_997_bind_error": "Could not bind to TCP port 997. Port in use or additional configuration required. See component documentation at: https://home-assistant.io/components/media_player.ps4/" + } + } +} diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py new file mode 100644 index 00000000000..51260f5d86e --- /dev/null +++ b/homeassistant/components/ps4/__init__.py @@ -0,0 +1,33 @@ +""" +Support for PlayStation 4 consoles. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ps4/ +""" +import logging + +from homeassistant.components.ps4.config_flow import PlayStation4FlowHandler # noqa: pylint: disable=unused-import +from homeassistant.components.ps4.const import DOMAIN # noqa: pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['pyps4-homeassistant==0.3.0'] + + +async def async_setup(hass, config): + """Set up the PS4 Component.""" + return True + + +async def async_setup_entry(hass, config_entry): + """Set up PS4 from a config entry.""" + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + config_entry, 'media_player')) + return True + + +async def async_unload_entry(hass, entry): + """Unload a PS4 config entry.""" + await hass.config_entries.async_forward_entry_unload( + entry, 'media_player') + return True diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py new file mode 100644 index 00000000000..3557c3fd930 --- /dev/null +++ b/homeassistant/components/ps4/config_flow.py @@ -0,0 +1,123 @@ +"""Config Flow for PlayStation 4.""" +from collections import OrderedDict +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.ps4.const import ( + DEFAULT_NAME, DEFAULT_REGION, DOMAIN, REGIONS) +from homeassistant.const import ( + CONF_CODE, CONF_HOST, CONF_IP_ADDRESS, CONF_NAME, CONF_REGION, CONF_TOKEN) + +_LOGGER = logging.getLogger(__name__) + +UDP_PORT = 987 +TCP_PORT = 997 +PORT_MSG = {UDP_PORT: 'port_987_bind_error', TCP_PORT: 'port_997_bind_error'} + + +@config_entries.HANDLERS.register(DOMAIN) +class PlayStation4FlowHandler(config_entries.ConfigFlow): + """Handle a PlayStation 4 config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the config flow.""" + from pyps4_homeassistant import Helper + + self.helper = Helper() + self.creds = None + self.name = None + self.host = None + self.region = None + self.pin = None + + async def async_step_user(self, user_input=None): + """Handle a user config flow.""" + # Abort if device is configured. + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason='devices_configured') + + # Check if able to bind to ports: UDP 987, TCP 997. + ports = PORT_MSG.keys() + failed = await self.hass.async_add_executor_job( + self.helper.port_bind, ports) + if failed in ports: + reason = PORT_MSG[failed] + return self.async_abort(reason=reason) + return await self.async_step_creds() + + async def async_step_creds(self, user_input=None): + """Return PS4 credentials from 2nd Screen App.""" + if user_input is not None: + self.creds = await self.hass.async_add_executor_job( + self.helper.get_creds) + + if self.creds is not None: + return await self.async_step_link() + return self.async_abort(reason='credential_error') + + return self.async_show_form( + step_id='creds') + + async def async_step_link(self, user_input=None): + """Prompt user input. Create or edit entry.""" + errors = {} + + # Search for device. + devices = await self.hass.async_add_executor_job( + self.helper.has_devices) + + # Abort if can't find device. + if not devices: + return self.async_abort(reason='no_devices_found') + + device_list = [ + device['host-ip'] for device in devices] + + # Login to PS4 with user data. + if user_input is not None: + self.region = user_input[CONF_REGION] + self.name = user_input[CONF_NAME] + self.pin = user_input[CONF_CODE] + self.host = user_input[CONF_IP_ADDRESS] + + is_ready, is_login = await self.hass.async_add_executor_job( + self.helper.link, self.host, self.creds, self.pin) + + if is_ready is False: + errors['base'] = 'not_ready' + elif is_login is False: + errors['base'] = 'login_failed' + else: + device = { + CONF_HOST: self.host, + CONF_NAME: self.name, + CONF_REGION: self.region + } + + # Create entry. + return self.async_create_entry( + title='PlayStation 4', + data={ + CONF_TOKEN: self.creds, + 'devices': [device], + }, + ) + + # Show User Input form. + link_schema = OrderedDict() + link_schema[vol.Required(CONF_IP_ADDRESS)] = vol.In(list(device_list)) + link_schema[vol.Required( + CONF_REGION, default=DEFAULT_REGION)] = vol.In(list(REGIONS)) + link_schema[vol.Required(CONF_CODE)] = str + link_schema[vol.Required(CONF_NAME, default=DEFAULT_NAME)] = str + + return self.async_show_form( + step_id='link', + data_schema=vol.Schema(link_schema), + errors=errors, + ) diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py new file mode 100644 index 00000000000..0618ca9675f --- /dev/null +++ b/homeassistant/components/ps4/const.py @@ -0,0 +1,5 @@ +"""Constants for PlayStation 4.""" +DEFAULT_NAME = "PlayStation 4" +DEFAULT_REGION = "R1" +DOMAIN = 'ps4' +REGIONS = ('R1', 'R2', 'R3', 'R4', 'R5') diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py new file mode 100644 index 00000000000..bf7be1bbf91 --- /dev/null +++ b/homeassistant/components/ps4/media_player.py @@ -0,0 +1,372 @@ +""" +Support for PlayStation 4 consoles. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.ps4/ +""" +from datetime import timedelta +import logging +import socket + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +import homeassistant.util as util +from homeassistant.components.media_player import ( + MediaPlayerDevice, ENTITY_IMAGE_URL) +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, +) +from homeassistant.components.ps4.const import DOMAIN as PS4_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_COMMAND, CONF_HOST, CONF_NAME, CONF_REGION, + CONF_TOKEN, STATE_IDLE, STATE_OFF, STATE_PLAYING, +) +from homeassistant.util.json import load_json, save_json + + +DEPENDENCIES = ['ps4'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_PS4 = SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ + SUPPORT_STOP | SUPPORT_SELECT_SOURCE + +PS4_DATA = 'ps4_data' +ICON = 'mdi:playstation' +GAMES_FILE = '.ps4-games.json' +MEDIA_IMAGE_DEFAULT = None + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=10) + +COMMANDS = ( + 'up', + 'down', + 'right', + 'left', + 'enter', + 'back', + 'option', + 'ps', +) + +SERVICE_COMMAND = 'send_command' + +PS4_COMMAND_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_COMMAND): vol.In(list(COMMANDS)) +}) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up PS4 from a config entry.""" + config = config_entry + + def add_entities(entities, update_before_add=False): + """Sync version of async add devices.""" + hass.add_job(async_add_entities, entities, update_before_add) + + await hass.async_add_executor_job( + setup_platform, hass, config, + add_entities, None) + + async def async_service_handle(hass): + """Handle for services.""" + def service_command(call): + entity_ids = call.data[ATTR_ENTITY_ID] + command = call.data[ATTR_COMMAND] + for device in hass.data[PS4_DATA].devices: + if device.entity_id in entity_ids: + device.send_command(command) + + hass.services.async_register( + PS4_DOMAIN, SERVICE_COMMAND, service_command, + schema=PS4_COMMAND_SCHEMA) + + await async_service_handle(hass) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up PS4 Platform.""" + import pyps4_homeassistant as pyps4 + hass.data[PS4_DATA] = PS4Data() + games_file = hass.config.path(GAMES_FILE) + creds = config.data[CONF_TOKEN] + device_list = [] + for device in config.data['devices']: + host = device[CONF_HOST] + region = device[CONF_REGION] + name = device[CONF_NAME] + ps4 = pyps4.Ps4(host, creds) + device_list.append(PS4Device( + name, host, region, ps4, games_file)) + add_entities(device_list, True) + + +class PS4Data(): + """Init Data Class.""" + + def __init__(self): + """Init Class.""" + self.devices = [] + + +class PS4Device(MediaPlayerDevice): + """Representation of a PS4.""" + + def __init__(self, name, host, region, ps4, games_file): + """Initialize the ps4 device.""" + self._ps4 = ps4 + self._host = host + self._name = name + self._region = region + self._state = None + self._games_filename = games_file + self._media_content_id = None + self._media_title = None + self._media_image = None + self._source = None + self._games = {} + self._source_list = [] + self._retry = 0 + self._info = None + self._unique_id = None + + async def async_added_to_hass(self): + """Subscribe PS4 events.""" + self.hass.data[PS4_DATA].devices.append(self) + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + def update(self): + """Retrieve the latest data.""" + try: + status = self._ps4.get_status() + if self._info is None: + self.get_device_info(status) + self._games = self.load_games() + if self._games is not None: + self._source_list = list(sorted(self._games.values())) + except socket.timeout: + status = None + if status is not None: + self._retry = 0 + if status.get('status') == 'Ok': + title_id = status.get('running-app-titleid') + name = status.get('running-app-name') + if title_id and name is not None: + self._state = STATE_PLAYING + if self._media_content_id != title_id: + self._media_content_id = title_id + self.get_title_data(title_id, name) + else: + self.idle() + else: + self.state_off() + elif self._retry > 5: + self.state_unknown() + else: + self._retry += 1 + + def idle(self): + """Set states for state idle.""" + self.reset_title() + self._state = STATE_IDLE + + def state_off(self): + """Set states for state off.""" + self.reset_title() + self._state = STATE_OFF + + def state_unknown(self): + """Set states for state unknown.""" + self.reset_title() + self._state = None + _LOGGER.warning("PS4 could not be reached") + self._retry = 0 + + def reset_title(self): + """Update if there is no title.""" + self._media_title = None + self._media_content_id = None + self._source = None + + def get_title_data(self, title_id, name): + """Get PS Store Data.""" + app_name = None + art = None + try: + app_name, art = self._ps4.get_ps_store_data( + name, title_id, self._region) + except TypeError: + _LOGGER.error( + "Could not find data in region: %s for PS ID: %s", + self._region, title_id) + finally: + self._media_title = app_name or name + self._source = self._media_title + self._media_image = art + self.update_list() + + def update_list(self): + """Update Game List, Correct data if different.""" + if self._media_content_id in self._games: + store = self._games[self._media_content_id] + if store != self._media_title: + self._games.pop(self._media_content_id) + if self._media_content_id not in self._games: + self.add_games(self._media_content_id, self._media_title) + self._games = self.load_games() + self._source_list = list(sorted(self._games.values())) + + def load_games(self): + """Load games for sources.""" + g_file = self._games_filename + try: + games = load_json(g_file) + + # If file does not exist, create empty file. + except FileNotFoundError: + games = {} + self.save_games(games) + return games + + def save_games(self, games): + """Save games to file.""" + g_file = self._games_filename + try: + save_json(g_file, games) + except OSError as error: + _LOGGER.error("Could not save game list, %s", error) + + # Retry loading file + if games is None: + self.load_games() + + def add_games(self, title_id, app_name): + """Add games to list.""" + games = self._games + if title_id is not None and title_id not in games: + game = {title_id: app_name} + games.update(game) + self.save_games(games) + + def get_device_info(self, status): + """Return device info for registry.""" + _sw_version = status['system-version'] + _sw_version = _sw_version[1:4] + sw_version = "{}.{}".format(_sw_version[0], _sw_version[1:]) + self._info = { + 'name': status['host-name'], + 'model': 'PlayStation 4', + 'identifiers': { + (PS4_DOMAIN, status['host-id']) + }, + 'manufacturer': 'Sony Interactive Entertainment Inc.', + 'sw_version': sw_version + } + self._unique_id = status['host-id'] + + @property + def device_info(self): + """Return information about the device.""" + return self._info + + @property + def unique_id(self): + """Return Unique ID for entity.""" + return self._unique_id + + @property + def entity_picture(self): + """Return picture.""" + if self._state == STATE_PLAYING and self._media_content_id is not None: + image_hash = self.media_image_hash + if image_hash is not None: + return ENTITY_IMAGE_URL.format( + self.entity_id, self.access_token, image_hash) + return MEDIA_IMAGE_DEFAULT + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def icon(self): + """Icon.""" + return ICON + + @property + def media_content_id(self): + """Content ID of current playing media.""" + return self._media_content_id + + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def media_image_url(self): + """Image url of current playing media.""" + if self._media_content_id is None: + return MEDIA_IMAGE_DEFAULT + return self._media_image + + @property + def media_title(self): + """Title of current playing media.""" + return self._media_title + + @property + def supported_features(self): + """Media player features that are supported.""" + return SUPPORT_PS4 + + @property + def source(self): + """Return the current input source.""" + return self._source + + @property + def source_list(self): + """List of available input sources.""" + return self._source_list + + def turn_off(self): + """Turn off media player.""" + self._ps4.standby() + + def turn_on(self): + """Turn on the media player.""" + self._ps4.wakeup() + + def media_pause(self): + """Send keypress ps to return to menu.""" + self._ps4.remote_control('ps') + + def media_stop(self): + """Send keypress ps to return to menu.""" + self._ps4.remote_control('ps') + + def select_source(self, source): + """Select input source.""" + for title_id, game in self._games.items(): + if source == game: + _LOGGER.debug( + "Starting PS4 game %s (%s) using source %s", + game, title_id, source) + self._ps4.start_title( + title_id, running_id=self._media_content_id) + return + + def send_command(self, command): + """Send Button Command.""" + self._ps4.remote_control(command) diff --git a/homeassistant/components/ps4/services.yaml b/homeassistant/components/ps4/services.yaml new file mode 100644 index 00000000000..b7d1e8df96f --- /dev/null +++ b/homeassistant/components/ps4/services.yaml @@ -0,0 +1,9 @@ +send_command: + description: Emulate button press for PlayStation 4. + fields: + entity_id: + description: Name(s) of entities to send command. + example: 'media_player.playstation_4' + command: + description: Button to press. + example: 'ps' diff --git a/homeassistant/components/ps4/strings.json b/homeassistant/components/ps4/strings.json new file mode 100644 index 00000000000..5f4e2a7c8b4 --- /dev/null +++ b/homeassistant/components/ps4/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "title": "PlayStation 4", + "step": { + "creds": { + "title": "PlayStation 4", + "description": "Credentials needed. Press 'Submit' and then in the PS4 2nd Screen App, refresh devices and select the 'Home-Assistant' device to continue." + }, + "link": { + "title": "PlayStation 4", + "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed.", + "data": { + "region": "Region", + "name": "Name", + "code": "PIN", + "ip_address": "IP Address" + } + } + }, + "error": { + "not_ready": "PlayStation 4 is not on or connected to network.", + "login_failed": "Failed to pair to PlayStation 4. Verify PIN is correct." + }, + "abort": { + "credential_error": "Error fetching credentials.", + "no_devices_found": "No PlayStation 4 devices found on the network.", + "devices_configured": "All devices found are already configured.", + "port_987_bind_error": "Could not bind to port 987.", + "port_997_bind_error": "Could not bind to port 997." + } + } +} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index cb79f457ce5..7c2a3155557 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -165,6 +165,7 @@ FLOWS = [ 'openuv', 'owntracks', 'point', + 'ps4', 'rainmachine', 'simplisafe', 'smartthings', diff --git a/requirements_all.txt b/requirements_all.txt index d2bea39f4cf..3f7104d0810 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1207,6 +1207,9 @@ pypoint==1.1.1 # homeassistant.components.sensor.pollen pypollencom==2.2.2 +# homeassistant.components.ps4 +pyps4-homeassistant==0.3.0 + # homeassistant.components.qwikswitch pyqwikswitch==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6525bbda40d..c398f0b981e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,6 +210,9 @@ pyopenuv==1.0.4 # homeassistant.components.sensor.otp pyotp==2.2.6 +# homeassistant.components.ps4 +pyps4-homeassistant==0.3.0 + # homeassistant.components.qwikswitch pyqwikswitch==0.8 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 47028ef3530..46f111ded6c 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -90,6 +90,7 @@ TEST_REQUIREMENTS = ( 'pynx584', 'pyopenuv', 'pyotp', + 'pyps4-homeassistant', 'pysmartapp', 'pysmartthings', 'pysonos', diff --git a/tests/components/ps4/__init__.py b/tests/components/ps4/__init__.py new file mode 100644 index 00000000000..c80bcf9173d --- /dev/null +++ b/tests/components/ps4/__init__.py @@ -0,0 +1 @@ +"""Tests for the PlayStation 4 component.""" diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py new file mode 100644 index 00000000000..b0170beeb48 --- /dev/null +++ b/tests/components/ps4/test_config_flow.py @@ -0,0 +1,149 @@ +"""Define tests for the PlayStation 4 config flow.""" +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components import ps4 +from homeassistant.components.ps4.const import ( + DEFAULT_NAME, DEFAULT_REGION) +from homeassistant.const import ( + CONF_CODE, CONF_HOST, CONF_IP_ADDRESS, CONF_NAME, CONF_REGION, CONF_TOKEN) + +from tests.common import MockConfigEntry + +MOCK_TITLE = 'PlayStation 4' +MOCK_CODE = '12345678' +MOCK_CREDS = '000aa000' +MOCK_HOST = '192.0.0.0' +MOCK_DEVICE = { + CONF_HOST: MOCK_HOST, + CONF_NAME: DEFAULT_NAME, + CONF_REGION: DEFAULT_REGION +} +MOCK_CONFIG = { + CONF_IP_ADDRESS: MOCK_HOST, + CONF_NAME: DEFAULT_NAME, + CONF_REGION: DEFAULT_REGION, + CONF_CODE: MOCK_CODE +} +MOCK_DATA = { + CONF_TOKEN: MOCK_CREDS, + 'devices': MOCK_DEVICE +} +MOCK_UDP_PORT = int(987) +MOCK_TCP_PORT = int(997) + + +async def test_full_flow_implementation(hass): + """Test registering an implementation and flow works.""" + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + # User Step Started, results in Step Creds + with patch('pyps4_homeassistant.Helper.port_bind', + return_value=None): + result = await flow.async_step_user() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'creds' + + # Step Creds results with form in Step Link. + with patch('pyps4_homeassistant.Helper.get_creds', + return_value=MOCK_CREDS), \ + patch('pyps4_homeassistant.Helper.has_devices', + return_value=[{'host-ip': MOCK_HOST}]): + result = await flow.async_step_creds({}) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + + # User Input results in created entry. + with patch('pyps4_homeassistant.Helper.link', + return_value=(True, True)), \ + patch('pyps4_homeassistant.Helper.has_devices', + return_value=[{'host-ip': MOCK_HOST}]): + result = await flow.async_step_link(MOCK_CONFIG) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['data'][CONF_TOKEN] == MOCK_CREDS + assert result['data']['devices'] == [MOCK_DEVICE] + assert result['title'] == MOCK_TITLE + + +async def test_port_bind_abort(hass): + """Test that flow aborted when cannot bind to ports 987, 997.""" + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + with patch('pyps4_homeassistant.Helper.port_bind', + return_value=MOCK_UDP_PORT): + reason = 'port_987_bind_error' + result = await flow.async_step_user(user_input=None) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == reason + + with patch('pyps4_homeassistant.Helper.port_bind', + return_value=MOCK_TCP_PORT): + reason = 'port_997_bind_error' + result = await flow.async_step_user(user_input=None) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == reason + + +async def test_duplicate_abort(hass): + """Test that Flow aborts when already configured.""" + MockConfigEntry(domain=ps4.DOMAIN, data=MOCK_DATA).add_to_hass(hass) + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'devices_configured' + + +async def test_no_devices_found_abort(hass): + """Test that failure to find devices aborts flow.""" + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + with patch('pyps4_homeassistant.Helper.has_devices', return_value=None): + result = await flow.async_step_link(MOCK_CONFIG) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'no_devices_found' + + +async def test_credential_abort(hass): + """Test that failure to get credentials aborts flow.""" + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + with patch('pyps4_homeassistant.Helper.get_creds', return_value=None): + result = await flow.async_step_creds({}) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'credential_error' + + +async def test_invalid_pin_error(hass): + """Test that invalid pin throws an error.""" + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + with patch('pyps4_homeassistant.Helper.link', + return_value=(True, False)), \ + patch('pyps4_homeassistant.Helper.has_devices', + return_value=[{'host-ip': MOCK_HOST}]): + result = await flow.async_step_link(MOCK_CONFIG) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + assert result['errors'] == {'base': 'login_failed'} + + +async def test_device_connection_error(hass): + """Test that device not connected or on throws an error.""" + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + with patch('pyps4_homeassistant.Helper.link', + return_value=(False, True)), \ + patch('pyps4_homeassistant.Helper.has_devices', + return_value=[{'host-ip': MOCK_HOST}]): + result = await flow.async_step_link(MOCK_CONFIG) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'link' + assert result['errors'] == {'base': 'not_ready'} From 425b9851fc5afabf289b974b89e211cd7a9d4b29 Mon Sep 17 00:00:00 2001 From: lapy Date: Sun, 17 Feb 2019 22:38:04 +0000 Subject: [PATCH 051/253] Add traccar monitored_conditions option (#21149) * Add traccar monitored_conditions option User defined additional parameters to track from the traccar platform * Version bump for pytraccar client * Update traccar.py * Remove default value * Update homeassistant/components/device_tracker/traccar.py Co-Authored-By: lapy * Update traccar.py --- .../components/device_tracker/traccar.py | 20 ++++++++++++++----- requirements_all.txt | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/traccar.py b/homeassistant/components/device_tracker/traccar.py index 4d340100849..1447f7c896c 100644 --- a/homeassistant/components/device_tracker/traccar.py +++ b/homeassistant/components/device_tracker/traccar.py @@ -13,14 +13,14 @@ from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, CONF_PASSWORD, CONF_USERNAME, ATTR_BATTERY_LEVEL, - CONF_SCAN_INTERVAL) + CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify -REQUIREMENTS = ['pytraccar==0.2.1'] +REQUIREMENTS = ['pytraccar==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -41,6 +41,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PORT, default=8082): cv.port, vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Optional(CONF_MONITORED_CONDITIONS, + default=[]): vol.All(cv.ensure_list, [cv.string]), }) @@ -52,16 +54,21 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): api = API(hass.loop, session, config[CONF_USERNAME], config[CONF_PASSWORD], config[CONF_HOST], config[CONF_PORT], config[CONF_SSL]) + scanner = TraccarScanner( - api, hass, async_see, config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)) + api, hass, async_see, + config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), + config[CONF_MONITORED_CONDITIONS]) + return await scanner.async_init() class TraccarScanner: """Define an object to retrieve Traccar data.""" - def __init__(self, api, hass, async_see, scan_interval): + def __init__(self, api, hass, async_see, scan_interval, custom_attributes): """Initialize.""" + self._custom_attributes = custom_attributes self._scan_interval = scan_interval self._async_see = async_see self._api = api @@ -81,7 +88,7 @@ class TraccarScanner: async def _async_update(self, now=None): """Update info from Traccar.""" _LOGGER.debug('Updating device data.') - await self._api.get_device_info() + await self._api.get_device_info(self._custom_attributes) for devicename in self._api.device_info: device = self._api.device_info[devicename] attr = {} @@ -98,6 +105,9 @@ class TraccarScanner: attr[ATTR_BATTERY_LEVEL] = device['battery'] if device.get('motion') is not None: attr[ATTR_MOTION] = device['motion'] + for custom_attr in self._custom_attributes: + if device.get(custom_attr) is not None: + attr[custom_attr] = device[custom_attr] await self._async_see( dev_id=slugify(device['device_id']), gps=(device.get('latitude'), device.get('longitude')), diff --git a/requirements_all.txt b/requirements_all.txt index 3f7104d0810..61460db5394 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1403,7 +1403,7 @@ pytile==2.0.5 pytouchline==0.7 # homeassistant.components.device_tracker.traccar -pytraccar==0.2.1 +pytraccar==0.3.0 # homeassistant.components.device_tracker.trackr pytrackr==0.0.5 From ce7f678b9b505e25ab6d995bedd2fc6544e294f8 Mon Sep 17 00:00:00 2001 From: CV Date: Mon, 18 Feb 2019 03:53:57 +0100 Subject: [PATCH 052/253] RSSI_PEER and RSSI_DEVICE are different things (fixes #20900) (#20902) * Fix #20900: RSSI_PEER and RSSI_DEVICE are different things This change is fixing issue #20900. Wireless actors are having two RSSI values. The way the component was programmed one of them was overwritten. * Added deprecation comment * Fixed long line * Fix: pylint comment * Lint * flake8 * flake8 again * Update __init__.py --- homeassistant/components/homematic/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 2918f8e03f9..e71bc1d8f2d 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -107,8 +107,8 @@ HM_ATTRIBUTE_SUPPORT = { 'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}], 'ERROR_SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], 'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], - 'RSSI_PEER': ['rssi', {}], - 'RSSI_DEVICE': ['rssi', {}], + 'RSSI_PEER': ['rssi_peer', {}], + 'RSSI_DEVICE': ['rssi_device', {}], 'VALVE_STATE': ['valve', {}], 'LEVEL': ['level', {}], 'BATTERY_STATE': ['battery', {}], From 3b5ed7a20f99b6ba992e429e9c2d558483578891 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 18 Feb 2019 04:40:51 +0000 Subject: [PATCH 053/253] Fix track_change error in utility_meter (#21134) * split validation * remove any() --- homeassistant/components/utility_meter/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index a59d51d97e2..a01c53b20e3 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -83,9 +83,9 @@ class UtilityMeterSensor(RestoreEntity): @callback def async_reading(self, entity, old_state, new_state): """Handle the sensor state changes.""" - if any([old_state is None, - old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE], - new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]]): + if old_state is None or new_state is None or\ + old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] or\ + new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: return if self._unit_of_measurement is None and\ From 2b86fc3841011c8af071a59e5370b29587c1b7fb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 18 Feb 2019 11:18:40 +0100 Subject: [PATCH 054/253] Upgrade voluptuous-serialize to 2.1.0 (#21166) --- 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 d0a192a9dc7..775425eb58b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ pyyaml>=3.13,<4 requests==2.21.0 ruamel.yaml==0.15.88 voluptuous==0.11.5 -voluptuous-serialize==2.0.0 +voluptuous-serialize==2.1.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 61460db5394..c8729bf3fcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -15,7 +15,7 @@ pyyaml>=3.13,<4 requests==2.21.0 ruamel.yaml==0.15.88 voluptuous==0.11.5 -voluptuous-serialize==2.0.0 +voluptuous-serialize==2.1.0 # homeassistant.components.nuimo_controller --only-binary=all nuimo==0.1.0 diff --git a/setup.py b/setup.py index 52be310574a..3b2863e9921 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ REQUIRES = [ 'requests==2.21.0', 'ruamel.yaml==0.15.88', 'voluptuous==0.11.5', - 'voluptuous-serialize==2.0.0', + 'voluptuous-serialize==2.1.0', ] MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) From 7e855d5b48c7ab0b55351f57224b992dd5899b34 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 18 Feb 2019 11:19:21 +0100 Subject: [PATCH 055/253] Upgrade youtube_dl to 2019.02.18 (#21164) --- homeassistant/components/media_extractor/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index efc3e8bddc8..d0b3c9e3c00 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -12,7 +12,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2019.02.08'] +REQUIREMENTS = ['youtube_dl==2019.02.18'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index c8729bf3fcc..4e17dcb2816 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1793,7 +1793,7 @@ yeelight==0.4.3 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.02.08 +youtube_dl==2019.02.18 # homeassistant.components.light.zengge zengge==0.2 From 6cdc5a5c2d100a1316a01af27965eb7710fc1fa2 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 18 Feb 2019 11:19:40 +0100 Subject: [PATCH 056/253] Upgrade sqlalchemy to 1.2.18 (#21162) --- homeassistant/components/recorder/__init__.py | 2 +- homeassistant/components/sensor/sql.py | 9 ++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 9b852b4a00a..e0af36ea409 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -25,7 +25,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.17'] +REQUIREMENTS = ['sqlalchemy==1.2.18'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index f780158dd4e..bd246c0d01c 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -1,9 +1,4 @@ -""" -Sensor from an SQL Query. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.sql/ -""" +"""Sensor from an SQL Query.""" import decimal import datetime import logging @@ -20,7 +15,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.17'] +REQUIREMENTS = ['sqlalchemy==1.2.18'] CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' diff --git a/requirements_all.txt b/requirements_all.txt index 4e17dcb2816..32c8f3aad4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1605,7 +1605,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sensor.sql -sqlalchemy==1.2.17 +sqlalchemy==1.2.18 # homeassistant.components.sensor.srp_energy srpenergy==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c398f0b981e..df4112b71a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -279,7 +279,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sensor.sql -sqlalchemy==1.2.17 +sqlalchemy==1.2.18 # homeassistant.components.sensor.srp_energy srpenergy==1.0.5 From 1afbc22ad5541f25b23af5577b7296c091541673 Mon Sep 17 00:00:00 2001 From: John Mihalic <2854333+mezz64@users.noreply.github.com> Date: Mon, 18 Feb 2019 05:20:31 -0500 Subject: [PATCH 057/253] Update pyEight for Python 3.7 Compatability (#21161) --- homeassistant/components/eight_sleep/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 851fd3d1c31..ca6c8a5a5c6 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pyeight==0.1.0'] +REQUIREMENTS = ['pyeight==0.1.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 32c8f3aad4c..1824cef8f86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1004,7 +1004,7 @@ pyeconet==0.0.6 pyedimax==0.1 # homeassistant.components.eight_sleep -pyeight==0.1.0 +pyeight==0.1.1 # homeassistant.components.media_player.emby pyemby==1.6 From 4d410bf5b927a84a1b0ecafbf9512e9cf6cfe6a1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 18 Feb 2019 15:51:21 +0100 Subject: [PATCH 058/253] Upgrade psutil to 5.5.1 (#21171) --- homeassistant/components/sensor/systemmonitor.py | 9 ++------- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index d0b3df5dd0e..70fb1f91051 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -1,9 +1,4 @@ -""" -Support for monitoring the local system. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.systemmonitor/ -""" +"""Support for monitoring the local system.""" import logging import os import socket @@ -16,7 +11,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['psutil==5.5.0'] +REQUIREMENTS = ['psutil==5.5.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1824cef8f86..90e6e2b9c34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -853,7 +853,7 @@ prometheus_client==0.2.0 protobuf==3.6.1 # homeassistant.components.sensor.systemmonitor -psutil==5.5.0 +psutil==5.5.1 # homeassistant.components.wink pubnubsub-handler==1.0.3 From 0b77a89a2ffab836f62d5a6a6abcc3f27d7ef30a Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Mon, 18 Feb 2019 15:54:23 +0100 Subject: [PATCH 059/253] Fix HomematicIP Cloud fix cover position property (#21154) --- homeassistant/components/homematicip_cloud/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 7675929d40f..86c11dab70d 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -39,7 +39,7 @@ class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): @property def current_cover_position(self): """Return current position of cover.""" - return int(self._device.shutterLevel * 100) + return int((1 - self._device.shutterLevel) * 100) async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" From 0ab9b006f0f500b2ef33f9afd9f380edea9e85e3 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 18 Feb 2019 15:58:25 +0100 Subject: [PATCH 060/253] Clean up upc_connect tests (#21150) --- .../device_tracker/test_upc_connect.py | 391 ++++++++---------- 1 file changed, 174 insertions(+), 217 deletions(-) diff --git a/tests/components/device_tracker/test_upc_connect.py b/tests/components/device_tracker/test_upc_connect.py index 6b38edc3ce9..677a6d1f310 100644 --- a/tests/components/device_tracker/test_upc_connect.py +++ b/tests/components/device_tracker/test_upc_connect.py @@ -1,273 +1,230 @@ """The tests for the UPC ConnextBox device tracker platform.""" import asyncio -from unittest.mock import patch -import logging +from asynctest import patch import pytest -from homeassistant.setup import setup_component -from homeassistant.const import ( - CONF_PLATFORM, CONF_HOST) from homeassistant.components.device_tracker import DOMAIN import homeassistant.components.device_tracker.upc_connect as platform -from homeassistant.util.async_ import run_coroutine_threadsafe +from homeassistant.const import CONF_HOST, CONF_PLATFORM +from homeassistant.setup import async_setup_component -from tests.common import ( - get_test_home_assistant, assert_setup_component, load_fixture, - mock_component, mock_coro) +from tests.common import assert_setup_component, load_fixture, mock_component -_LOGGER = logging.getLogger(__name__) +HOST = "127.0.0.1" -@asyncio.coroutine -def async_scan_devices_mock(scanner): +async def async_scan_devices_mock(scanner): """Mock async_scan_devices.""" return [] @pytest.fixture(autouse=True) -def mock_load_config(): - """Mock device tracker loading config.""" - with patch('homeassistant.components.device_tracker.async_load_config', - return_value=mock_coro([])): - yield +def setup_comp_deps(hass, mock_device_tracker_conf): + """Set up component dependencies.""" + mock_component(hass, 'zone') + mock_component(hass, 'group') + yield -class TestUPCConnect: - """Tests for the Ddwrt device tracker platform.""" +async def test_setup_platform_timeout_loginpage(hass, caplog, aioclient_mock): + """Set up a platform with timeout on loginpage.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + exc=asyncio.TimeoutError() + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + content=b'successful', + ) - def setup_method(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - mock_component(self.hass, 'zone') - mock_component(self.hass, 'group') + assert await async_setup_component( + hass, DOMAIN, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) - self.host = "127.0.0.1" + assert len(aioclient_mock.mock_calls) == 1 - def teardown_method(self): - """Stop everything that was started.""" - self.hass.stop() + assert 'Error setting up platform' in caplog.text - @patch('homeassistant.components.device_tracker.upc_connect.' - 'UPCDeviceScanner.async_scan_devices', - return_value=async_scan_devices_mock) - def test_setup_platform(self, scan_mock, aioclient_mock): - """Set up a platform.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - content=b'successful' - ) - with assert_setup_component(1, DOMAIN): - assert setup_component( - self.hass, DOMAIN, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }}) +async def test_setup_platform_timeout_webservice(hass, caplog, aioclient_mock): + """Set up a platform with api timeout.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'}, + content=b'successful', + exc=asyncio.TimeoutError() + ) - assert len(aioclient_mock.mock_calls) == 1 + assert await async_setup_component( + hass, DOMAIN, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) - @patch('homeassistant.components.device_tracker._LOGGER.error') - def test_setup_platform_timeout_webservice(self, mock_error, - aioclient_mock): - """Set up a platform with api timeout.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'}, - content=b'successful', - exc=asyncio.TimeoutError() - ) + assert len(aioclient_mock.mock_calls) == 1 - with assert_setup_component(1, DOMAIN): - assert setup_component( - self.hass, DOMAIN, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }}) + assert 'Error setting up platform' in caplog.text - assert len(aioclient_mock.mock_calls) == 1 - assert 'Error setting up platform' in \ - str(mock_error.call_args_list[-1]) +@patch('homeassistant.components.device_tracker.upc_connect.' + 'UPCDeviceScanner.async_scan_devices', + return_value=async_scan_devices_mock) +async def test_setup_platform(scan_mock, hass, aioclient_mock): + """Set up a platform.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + content=b'successful' + ) - @patch('homeassistant.components.device_tracker._LOGGER.error') - def test_setup_platform_timeout_loginpage(self, mock_error, - aioclient_mock): - """Set up a platform with timeout on loginpage.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - exc=asyncio.TimeoutError() - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - content=b'successful', - ) + with assert_setup_component(1, DOMAIN): + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: { + CONF_PLATFORM: 'upc_connect', + CONF_HOST: HOST + }}) - with assert_setup_component(1, DOMAIN): - assert setup_component( - self.hass, DOMAIN, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }}) + assert len(aioclient_mock.mock_calls) == 1 - assert len(aioclient_mock.mock_calls) == 1 - assert 'Error setting up platform' in \ - str(mock_error.call_args_list[-1]) +async def test_scan_devices(hass, aioclient_mock): + """Set up a upc platform and scan device.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + content=b'successful', + cookies={'sessionToken': '654321'} + ) - def test_scan_devices(self, aioclient_mock): - """Set up a upc platform and scan device.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - content=b'successful', - cookies={'sessionToken': '654321'} - ) + scanner = await platform.async_get_scanner( + hass, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) - scanner = run_coroutine_threadsafe(platform.async_get_scanner( - self.hass, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }} - ), self.hass.loop).result() + assert len(aioclient_mock.mock_calls) == 1 - assert len(aioclient_mock.mock_calls) == 1 + aioclient_mock.clear_requests() + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + text=load_fixture('upc_connect.xml'), + cookies={'sessionToken': '1235678'} + ) - aioclient_mock.clear_requests() - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - text=load_fixture('upc_connect.xml'), - cookies={'sessionToken': '1235678'} - ) + mac_list = await scanner.async_scan_devices() - mac_list = run_coroutine_threadsafe( - scanner.async_scan_devices(), self.hass.loop).result() + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == 'token=654321&fun=123' + assert mac_list == ['30:D3:2D:0:69:21', '5C:AA:FD:25:32:02', + '70:EE:50:27:A1:38'] - assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[0][2] == 'token=654321&fun=123' - assert mac_list == ['30:D3:2D:0:69:21', '5C:AA:FD:25:32:02', - '70:EE:50:27:A1:38'] - def test_scan_devices_without_session(self, aioclient_mock): - """Set up a upc platform and scan device with no token.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - content=b'successful', - cookies={'sessionToken': '654321'} - ) +async def test_scan_devices_without_session(hass, aioclient_mock): + """Set up a upc platform and scan device with no token.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + content=b'successful', + cookies={'sessionToken': '654321'} + ) - scanner = run_coroutine_threadsafe(platform.async_get_scanner( - self.hass, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }} - ), self.hass.loop).result() + scanner = await platform.async_get_scanner( + hass, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) - assert len(aioclient_mock.mock_calls) == 1 + assert len(aioclient_mock.mock_calls) == 1 - aioclient_mock.clear_requests() - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - text=load_fixture('upc_connect.xml'), - cookies={'sessionToken': '1235678'} - ) + aioclient_mock.clear_requests() + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + text=load_fixture('upc_connect.xml'), + cookies={'sessionToken': '1235678'} + ) - scanner.token = None - mac_list = run_coroutine_threadsafe( - scanner.async_scan_devices(), self.hass.loop).result() + scanner.token = None + mac_list = await scanner.async_scan_devices() - assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2] == 'token=654321&fun=123' - assert mac_list == ['30:D3:2D:0:69:21', '5C:AA:FD:25:32:02', - '70:EE:50:27:A1:38'] + assert len(aioclient_mock.mock_calls) == 2 + assert aioclient_mock.mock_calls[1][2] == 'token=654321&fun=123' + assert mac_list == ['30:D3:2D:0:69:21', '5C:AA:FD:25:32:02', + '70:EE:50:27:A1:38'] - def test_scan_devices_without_session_wrong_re(self, aioclient_mock): - """Set up a upc platform and scan device with no token and wrong.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - content=b'successful', - cookies={'sessionToken': '654321'} - ) - scanner = run_coroutine_threadsafe(platform.async_get_scanner( - self.hass, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }} - ), self.hass.loop).result() +async def test_scan_devices_without_session_wrong_re(hass, aioclient_mock): + """Set up a upc platform and scan device with no token and wrong.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + content=b'successful', + cookies={'sessionToken': '654321'} + ) - assert len(aioclient_mock.mock_calls) == 1 + scanner = await platform.async_get_scanner( + hass, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) - aioclient_mock.clear_requests() - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - status=400, - cookies={'sessionToken': '1235678'} - ) + assert len(aioclient_mock.mock_calls) == 1 - scanner.token = None - mac_list = run_coroutine_threadsafe( - scanner.async_scan_devices(), self.hass.loop).result() + aioclient_mock.clear_requests() + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + status=400, + cookies={'sessionToken': '1235678'} + ) - assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2] == 'token=654321&fun=123' - assert mac_list == [] + scanner.token = None + mac_list = await scanner.async_scan_devices() - def test_scan_devices_parse_error(self, aioclient_mock): - """Set up a upc platform and scan device with parse error.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(self.host), - cookies={'sessionToken': '654321'} - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - content=b'successful', - cookies={'sessionToken': '654321'} - ) + assert len(aioclient_mock.mock_calls) == 2 + assert aioclient_mock.mock_calls[1][2] == 'token=654321&fun=123' + assert mac_list == [] - scanner = run_coroutine_threadsafe(platform.async_get_scanner( - self.hass, {DOMAIN: { - CONF_PLATFORM: 'upc_connect', - CONF_HOST: self.host - }} - ), self.hass.loop).result() - assert len(aioclient_mock.mock_calls) == 1 +async def test_scan_devices_parse_error(hass, aioclient_mock): + """Set up a upc platform and scan device with parse error.""" + aioclient_mock.get( + "http://{}/common_page/login.html".format(HOST), + cookies={'sessionToken': '654321'} + ) + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + content=b'successful', + cookies={'sessionToken': '654321'} + ) - aioclient_mock.clear_requests() - aioclient_mock.post( - "http://{}/xml/getter.xml".format(self.host), - text="Blablebla blabalble", - cookies={'sessionToken': '1235678'} - ) + scanner = await platform.async_get_scanner( + hass, { + DOMAIN: {CONF_PLATFORM: 'upc_connect', CONF_HOST: HOST}}) - mac_list = run_coroutine_threadsafe( - scanner.async_scan_devices(), self.hass.loop).result() + assert len(aioclient_mock.mock_calls) == 1 - assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[0][2] == 'token=654321&fun=123' - assert scanner.token is None - assert mac_list == [] + aioclient_mock.clear_requests() + aioclient_mock.post( + "http://{}/xml/getter.xml".format(HOST), + text="Blablebla blabalble", + cookies={'sessionToken': '1235678'} + ) + + mac_list = await scanner.async_scan_devices() + + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == 'token=654321&fun=123' + assert scanner.token is None + assert mac_list == [] From 3f9e6a706406716f660c362bb0c7977b571fd9b4 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 18 Feb 2019 10:55:41 -0500 Subject: [PATCH 061/253] Add power source to device and clean up zha listeners (#21174) check available and add comments ensure order on API test --- homeassistant/components/zha/core/const.py | 3 +- homeassistant/components/zha/core/device.py | 12 +- homeassistant/components/zha/core/gateway.py | 64 +++++++--- .../components/zha/core/listeners.py | 111 ++++++++++++++---- homeassistant/components/zha/sensor.py | 5 +- tests/components/zha/common.py | 4 +- tests/components/zha/test_api.py | 14 ++- tests/components/zha/test_binary_sensor.py | 8 +- tests/components/zha/test_fan.py | 3 +- tests/components/zha/test_light.py | 6 +- tests/components/zha/test_sensor.py | 3 +- tests/components/zha/test_switch.py | 4 +- 12 files changed, 179 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 5edcadc7fce..faa423d8ac4 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -62,7 +62,6 @@ ILLUMINANCE = 'illuminance' PRESSURE = 'pressure' METERING = 'metering' ELECTRICAL_MEASUREMENT = 'electrical_measurement' -POWER_CONFIGURATION = 'power_configuration' GENERIC = 'generic' UNKNOWN = 'unknown' OPENING = 'opening' @@ -73,6 +72,7 @@ ATTR_LEVEL = 'level' LISTENER_ON_OFF = 'on_off' LISTENER_ATTRIBUTE = 'attribute' +LISTENER_BASIC = 'basic' LISTENER_COLOR = 'color' LISTENER_FAN = 'fan' LISTENER_LEVEL = ATTR_LEVEL @@ -113,6 +113,7 @@ CLUSTER_REPORT_CONFIGS = {} CUSTOM_CLUSTER_MAPPINGS = {} COMPONENT_CLUSTERS = {} EVENT_RELAY_CLUSTERS = [] +NO_SENSOR_CLUSTERS = [] REPORT_CONFIG_MAX_INT = 900 REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800 diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 7c972988e9c..7bb39f943f6 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -15,9 +15,9 @@ from .const import ( ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE, ATTR_ARGS, CLIENT_COMMANDS, SERVER_COMMANDS, ATTR_ENDPOINT_ID, IEEE, MODEL, NAME, UNKNOWN, QUIRK_APPLIED, - QUIRK_CLASS + QUIRK_CLASS, LISTENER_BASIC ) -from .listeners import EventRelayListener +from .listeners import EventRelayListener, BasicListener _LOGGER = logging.getLogger(__name__) @@ -59,6 +59,7 @@ class ZHADevice: self._zigpy_device.__class__.__module__, self._zigpy_device.__class__.__name__ ) + self.power_source = None @property def name(self): @@ -177,6 +178,13 @@ class ZHADevice: """Initialize listeners.""" _LOGGER.debug('%s: started initialization', self.name) await self._execute_listener_tasks('async_initialize', from_cache) + self.power_source = self.cluster_listeners.get( + LISTENER_BASIC).get_power_source() + _LOGGER.debug( + '%s: power source: %s', + self.name, + BasicListener.POWER_SOURCES.get(self.power_source) + ) _LOGGER.debug('%s: completed initialization', self.name) async def _execute_listener_tasks(self, task_name, *args): diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index ff3c374a850..391b12189cf 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -18,15 +18,15 @@ from .const import ( ZHA_DISCOVERY_NEW, DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, COMPONENT_CLUSTERS, HUMIDITY, TEMPERATURE, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT, - POWER_CONFIGURATION, GENERIC, SENSOR_TYPE, EVENT_RELAY_CLUSTERS, - LISTENER_BATTERY, UNKNOWN, OPENING, ZONE, OCCUPANCY, - CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_IMMEDIATE, REPORT_CONFIG_ASAP, - REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_OP, SIGNAL_REMOVE) + GENERIC, SENSOR_TYPE, EVENT_RELAY_CLUSTERS, LISTENER_BATTERY, UNKNOWN, + OPENING, ZONE, OCCUPANCY, CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_IMMEDIATE, + REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT, + REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, SIGNAL_REMOVE, NO_SENSOR_CLUSTERS) from .device import ZHADevice from ..device_entity import ZhaDeviceEntity from .listeners import ( - LISTENER_REGISTRY, AttributeListener, EventRelayListener, ZDOListener) + LISTENER_REGISTRY, AttributeListener, EventRelayListener, ZDOListener, + BasicListener) from .helpers import convert_ieee _LOGGER = logging.getLogger(__name__) @@ -165,7 +165,21 @@ class ZHAGateway: await self._component.async_add_entities([device_entity]) if is_new_join: + # because it's a new join we can immediately mark the device as + # available and we already loaded fresh state above zha_device.update_available(True) + elif not zha_device.available and zha_device.power_source is not None\ + and zha_device.power_source != BasicListener.BATTERY: + # the device is currently marked unavailable and it isn't a battery + # powered device so we should be able to update it now + _LOGGER.debug( + "attempting to request fresh state for %s %s", + zha_device.name, + "with power source: {}".format( + BasicListener.POWER_SOURCES.get(zha_device.power_source) + ) + ) + await zha_device.async_initialize(from_cache=False) async def _async_process_endpoint( self, endpoint_id, endpoint, discovery_infos, device, zha_device, @@ -312,6 +326,13 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, is_new_join, )) + if cluster.cluster_id in NO_SENSOR_CLUSTERS: + cluster_match_tasks.append(_handle_listener_only_cluster_match( + zha_device, + cluster, + is_new_join, + )) + for cluster in endpoint.out_clusters.values(): if cluster.cluster_id not in profile_clusters[1]: cluster_match_tasks.append(_handle_single_cluster_match( @@ -338,6 +359,12 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, return cluster_matches +async def _handle_listener_only_cluster_match( + zha_device, cluster, is_new_join): + """Handle a listener only cluster match.""" + await _create_cluster_listener(cluster, zha_device, is_new_join) + + async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, device_classes, is_new_join): """Dispatch a single cluster match to a HA component.""" @@ -352,11 +379,6 @@ async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, listeners = [] await _create_cluster_listener(cluster, zha_device, is_new_join, listeners=listeners) - # don't actually create entities for PowerConfiguration - # find a better way to do this without abusing single cluster reg - from zigpy.zcl.clusters.general import PowerConfiguration - if cluster.cluster_id == PowerConfiguration.cluster_id: - return cluster_key = "{}-{}".format(device_key, cluster.cluster_id) discovery_info = { @@ -405,6 +427,10 @@ def establish_device_mappings(): EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) + NO_SENSOR_CLUSTERS.append(zcl.clusters.general.Basic.cluster_id) + NO_SENSOR_CLUSTERS.append( + zcl.clusters.general.PowerConfiguration.cluster_id) + DEVICE_CLASS[zha.PROFILE_ID].update({ zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor', zha.DeviceType.LEVEL_CONTROL_SWITCH: 'binary_sensor', @@ -442,7 +468,6 @@ def establish_device_mappings(): zcl.clusters.measurement.IlluminanceMeasurement: 'sensor', zcl.clusters.smartenergy.Metering: 'sensor', zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor', - zcl.clusters.general.PowerConfiguration: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.measurement.OccupancySensing: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', @@ -462,8 +487,6 @@ def establish_device_mappings(): zcl.clusters.smartenergy.Metering.cluster_id: METERING, zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: ELECTRICAL_MEASUREMENT, - zcl.clusters.general.PowerConfiguration.cluster_id: - POWER_CONFIGURATION, }) BINARY_SENSOR_TYPES.update({ @@ -473,6 +496,19 @@ def establish_device_mappings(): }) CLUSTER_REPORT_CONFIGS.update({ + zcl.clusters.general.Alarms.cluster_id: [], + zcl.clusters.general.Basic.cluster_id: [], + zcl.clusters.general.Commissioning.cluster_id: [], + zcl.clusters.general.Identify.cluster_id: [], + zcl.clusters.general.Groups.cluster_id: [], + zcl.clusters.general.Scenes.cluster_id: [], + zcl.clusters.general.Partition.cluster_id: [], + zcl.clusters.general.Ota.cluster_id: [], + zcl.clusters.general.PowerProfile.cluster_id: [], + zcl.clusters.general.ApplianceControl.cluster_id: [], + zcl.clusters.general.PollControl.cluster_id: [], + zcl.clusters.general.GreenPowerProxy.cluster_id: [], + zcl.clusters.general.OnOffConfiguration.cluster_id: [], zcl.clusters.general.OnOff.cluster_id: [{ 'attr': 'on_off', 'config': REPORT_CONFIG_IMMEDIATE diff --git a/homeassistant/components/zha/core/listeners.py b/homeassistant/components/zha/core/listeners.py index 1b240d499b4..f8d24ce903c 100644 --- a/homeassistant/components/zha/core/listeners.py +++ b/homeassistant/components/zha/core/listeners.py @@ -18,7 +18,10 @@ from .helpers import ( safe_read, get_attr_id_by_name, bind_cluster) from .const import ( CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_DEFAULT, SIGNAL_ATTR_UPDATED, - SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, SIGNAL_STATE_ATTR, ATTR_LEVEL + SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, SIGNAL_STATE_ATTR, LISTENER_BASIC, + LISTENER_ATTRIBUTE, LISTENER_ON_OFF, LISTENER_COLOR, LISTENER_FAN, + LISTENER_LEVEL, LISTENER_ZONE, LISTENER_ACTIVE_POWER, LISTENER_BATTERY, + LISTENER_EVENT_RELAY ) LISTENER_REGISTRY = {} @@ -30,12 +33,25 @@ def populate_listener_registry(): """Populate the listener registry.""" from zigpy import zcl LISTENER_REGISTRY.update({ + zcl.clusters.general.Alarms.cluster_id: ClusterListener, + zcl.clusters.general.Commissioning.cluster_id: ClusterListener, + zcl.clusters.general.Identify.cluster_id: ClusterListener, + zcl.clusters.general.Groups.cluster_id: ClusterListener, + zcl.clusters.general.Scenes.cluster_id: ClusterListener, + zcl.clusters.general.Partition.cluster_id: ClusterListener, + zcl.clusters.general.Ota.cluster_id: ClusterListener, + zcl.clusters.general.PowerProfile.cluster_id: ClusterListener, + zcl.clusters.general.ApplianceControl.cluster_id: ClusterListener, + zcl.clusters.general.PollControl.cluster_id: ClusterListener, + zcl.clusters.general.GreenPowerProxy.cluster_id: ClusterListener, + zcl.clusters.general.OnOffConfiguration.cluster_id: ClusterListener, zcl.clusters.general.OnOff.cluster_id: OnOffListener, zcl.clusters.general.LevelControl.cluster_id: LevelListener, zcl.clusters.lighting.Color.cluster_id: ColorListener, zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: ActivePowerListener, zcl.clusters.general.PowerConfiguration.cluster_id: BatteryListener, + zcl.clusters.general.Basic.cluster_id: BasicListener, zcl.clusters.security.IasZone.cluster_id: IASZoneListener, zcl.clusters.hvac.Fan.cluster_id: FanListener, }) @@ -92,6 +108,7 @@ class ClusterListener: def __init__(self, cluster, device): """Initialize ClusterListener.""" + self.name = 'cluster_{}'.format(cluster.cluster_id) self._cluster = cluster self._zha_device = device self._unique_id = construct_unique_id(cluster) @@ -216,11 +233,10 @@ class ClusterListener: class AttributeListener(ClusterListener): """Listener for the attribute reports cluster.""" - name = 'attribute' - def __init__(self, cluster, device): """Initialize AttributeListener.""" super().__init__(cluster, device) + self.name = LISTENER_ATTRIBUTE attr = self._report_config[0].get('attr') if isinstance(attr, str): self._value_attribute = get_attr_id_by_name(self.cluster, attr) @@ -247,13 +263,12 @@ class AttributeListener(ClusterListener): class OnOffListener(ClusterListener): """Listener for the OnOff Zigbee cluster.""" - name = 'on_off' - ON_OFF = 0 def __init__(self, cluster, device): - """Initialize ClusterListener.""" + """Initialize OnOffListener.""" super().__init__(cluster, device) + self.name = LISTENER_ON_OFF self._state = None @callback @@ -295,10 +310,13 @@ class OnOffListener(ClusterListener): class LevelListener(ClusterListener): """Listener for the LevelControl Zigbee cluster.""" - name = ATTR_LEVEL - CURRENT_LEVEL = 0 + def __init__(self, cluster, device): + """Initialize LevelListener.""" + super().__init__(cluster, device) + self.name = LISTENER_LEVEL + @callback def cluster_command(self, tsn, command_id, args): """Handle commands received to this cluster.""" @@ -350,7 +368,10 @@ class LevelListener(ClusterListener): class IASZoneListener(ClusterListener): """Listener for the IASZone Zigbee cluster.""" - name = 'zone' + def __init__(self, cluster, device): + """Initialize LevelListener.""" + super().__init__(cluster, device) + self.name = LISTENER_ZONE @callback def cluster_command(self, tsn, command_id, args): @@ -415,7 +436,10 @@ class IASZoneListener(ClusterListener): class ActivePowerListener(AttributeListener): """Listener that polls active power level.""" - name = 'active_power' + def __init__(self, cluster, device): + """Initialize ActivePowerListener.""" + super().__init__(cluster, device) + self.name = LISTENER_ACTIVE_POWER async def async_update(self): """Retrieve latest state.""" @@ -423,7 +447,7 @@ class ActivePowerListener(AttributeListener): # This is a polling listener. Don't allow cache. result = await self.get_attribute_value( - 'active_power', from_cache=False) + LISTENER_ACTIVE_POWER, from_cache=False) async_dispatcher_send( self._zha_device.hass, "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), @@ -433,14 +457,53 @@ class ActivePowerListener(AttributeListener): async def async_initialize(self, from_cache): """Initialize listener.""" await self.get_attribute_value( - 'active_power', from_cache=from_cache) + LISTENER_ACTIVE_POWER, from_cache=from_cache) await super().async_initialize(from_cache) +class BasicListener(ClusterListener): + """Listener to interact with the basic cluster.""" + + BATTERY = 3 + POWER_SOURCES = { + 0: 'Unknown', + 1: 'Mains (single phase)', + 2: 'Mains (3 phase)', + BATTERY: 'Battery', + 4: 'DC source', + 5: 'Emergency mains constantly powered', + 6: 'Emergency mains and transfer switch' + } + + def __init__(self, cluster, device): + """Initialize BasicListener.""" + super().__init__(cluster, device) + self.name = LISTENER_BASIC + self._power_source = None + + async def async_configure(self): + """Configure this listener.""" + await super().async_configure() + await self.async_initialize(False) + + async def async_initialize(self, from_cache): + """Initialize listener.""" + self._power_source = await self.get_attribute_value( + 'power_source', from_cache=from_cache) + await super().async_initialize(from_cache) + + def get_power_source(self): + """Get the power source.""" + return self._power_source + + class BatteryListener(ClusterListener): """Listener that polls active power level.""" - name = 'battery' + def __init__(self, cluster, device): + """Initialize BatteryListener.""" + super().__init__(cluster, device) + self.name = LISTENER_BATTERY @callback def attribute_updated(self, attrid, value): @@ -480,7 +543,10 @@ class BatteryListener(ClusterListener): class EventRelayListener(ClusterListener): """Event relay that can be attached to zigbee clusters.""" - name = 'event_relay' + def __init__(self, cluster, device): + """Initialize EventRelayListener.""" + super().__init__(cluster, device) + self.name = LISTENER_EVENT_RELAY @callback def attribute_updated(self, attrid, value): @@ -512,15 +578,14 @@ class EventRelayListener(ClusterListener): class ColorListener(ClusterListener): """Color listener.""" - name = 'color' - CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 UNSUPPORTED_ATTRIBUTE = 0x86 def __init__(self, cluster, device): - """Initialize ClusterListener.""" + """Initialize ColorListener.""" super().__init__(cluster, device) + self.name = LISTENER_COLOR self._color_capabilities = None def get_color_capabilities(self): @@ -550,10 +615,13 @@ class ColorListener(ClusterListener): class FanListener(ClusterListener): """Fan listener.""" - name = 'fan' - _value_attribute = 0 + def __init__(self, cluster, device): + """Initialize FanListener.""" + super().__init__(cluster, device) + self.name = LISTENER_FAN + async def async_set_speed(self, value) -> None: """Set the speed of the fan.""" from zigpy.exceptions import DeliveryError @@ -595,10 +663,9 @@ class FanListener(ClusterListener): class ZDOListener: """Listener for ZDO events.""" - name = 'zdo' - def __init__(self, cluster, device): - """Initialize ClusterListener.""" + """Initialize ZDOListener.""" + self.name = 'zdo' self._cluster = cluster self._zha_device = device self._status = ListenerStatus.CREATED diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index ad566df00f4..9c00d8124bb 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -12,8 +12,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT, - POWER_CONFIGURATION, GENERIC, SENSOR_TYPE, LISTENER_ATTRIBUTE, - LISTENER_ACTIVE_POWER, SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR) + GENERIC, SENSOR_TYPE, LISTENER_ATTRIBUTE, LISTENER_ACTIVE_POWER, + SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR) from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -71,7 +71,6 @@ UNIT_REGISTRY = { ILLUMINANCE: 'lx', METERING: 'W', ELECTRICAL_MEASUREMENT: 'W', - POWER_CONFIGURATION: '%', GENERIC: None } diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index f0e1aa701e7..cd2eb53c3fe 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -174,6 +174,7 @@ async def async_test_device_join( only trigger during device joins can be tested. """ from zigpy.zcl.foundation import Status + from zigpy.zcl.clusters.general import Basic # create zigpy device mocking out the zigbee network operations with patch( 'zigpy.zcl.Cluster.configure_reporting', @@ -182,7 +183,8 @@ async def async_test_device_join( 'zigpy.zcl.Cluster.bind', return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): zigpy_device = await async_init_zigpy_device( - hass, [cluster_id], [], device_type, zha_gateway, + hass, [cluster_id, Basic.cluster_id], [], device_type, + zha_gateway, ieee="00:0d:6f:00:0a:90:69:f7", manufacturer="FakeMan{}".format(cluster_id), model="FakeMod{}".format(cluster_id), diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index ad139d81ddf..616a94e8b89 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -17,14 +17,14 @@ from .common import async_init_zigpy_device @pytest.fixture async def zha_client(hass, config_entry, zha_gateway, hass_ws_client): """Test zha switch platform.""" - from zigpy.zcl.clusters.general import OnOff + from zigpy.zcl.clusters.general import OnOff, Basic # load the ZHA API async_load_api(hass, Mock(), zha_gateway) # create zigpy device await async_init_zigpy_device( - hass, [OnOff.cluster_id], [], None, zha_gateway) + hass, [OnOff.cluster_id, Basic.cluster_id], [], None, zha_gateway) # load up switch domain await hass.config_entries.async_forward_entry_setup( @@ -44,10 +44,16 @@ async def test_device_clusters(hass, config_entry, zha_gateway, zha_client): msg = await zha_client.receive_json() - assert len(msg['result']) == 1 + assert len(msg['result']) == 2 - cluster_info = msg['result'][0] + cluster_infos = sorted(msg['result'], key=lambda k: k[ID]) + cluster_info = cluster_infos[0] + assert cluster_info[TYPE] == IN + assert cluster_info[ID] == 0 + assert cluster_info[NAME] == 'Basic' + + cluster_info = cluster_infos[1] assert cluster_info[TYPE] == IN assert cluster_info[ID] == 6 assert cluster_info[NAME] == 'OnOff' diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index c81f96468ce..d0763b8fb10 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -11,13 +11,13 @@ async def test_binary_sensor(hass, config_entry, zha_gateway): """Test zha binary_sensor platform.""" from zigpy.zcl.clusters.security import IasZone from zigpy.zcl.clusters.measurement import OccupancySensing - from zigpy.zcl.clusters.general import OnOff, LevelControl + from zigpy.zcl.clusters.general import OnOff, LevelControl, Basic from zigpy.profiles.zha import DeviceType # create zigpy devices zigpy_device_zone = await async_init_zigpy_device( hass, - [IasZone.cluster_id], + [IasZone.cluster_id, Basic.cluster_id], [], None, zha_gateway @@ -25,7 +25,7 @@ async def test_binary_sensor(hass, config_entry, zha_gateway): zigpy_device_remote = await async_init_zigpy_device( hass, - [], + [Basic.cluster_id], [OnOff.cluster_id, LevelControl.cluster_id], DeviceType.LEVEL_CONTROL_SWITCH, zha_gateway, @@ -36,7 +36,7 @@ async def test_binary_sensor(hass, config_entry, zha_gateway): zigpy_device_occupancy = await async_init_zigpy_device( hass, - [OccupancySensing.cluster_id], + [OccupancySensing.cluster_id, Basic.cluster_id], [], None, zha_gateway, diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 6beafc6ca8e..a70e0e5ea40 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -17,11 +17,12 @@ from .common import ( async def test_fan(hass, config_entry, zha_gateway): """Test zha fan platform.""" from zigpy.zcl.clusters.hvac import Fan + from zigpy.zcl.clusters.general import Basic from zigpy.zcl.foundation import Status # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Fan.cluster_id], [], None, zha_gateway) + hass, [Fan.cluster_id, Basic.cluster_id], [], None, zha_gateway) # load up fan domain await hass.config_entries.async_forward_entry_setup( diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 9c5e69d1347..38d7caedaad 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -14,13 +14,13 @@ OFF = 0 async def test_light(hass, config_entry, zha_gateway): """Test zha light platform.""" - from zigpy.zcl.clusters.general import OnOff, LevelControl + from zigpy.zcl.clusters.general import OnOff, LevelControl, Basic from zigpy.profiles.zha import DeviceType # create zigpy devices zigpy_device_on_off = await async_init_zigpy_device( hass, - [OnOff.cluster_id], + [OnOff.cluster_id, Basic.cluster_id], [], DeviceType.ON_OFF_LIGHT, zha_gateway @@ -28,7 +28,7 @@ async def test_light(hass, config_entry, zha_gateway): zigpy_device_level = await async_init_zigpy_device( hass, - [OnOff.cluster_id, LevelControl.cluster_id], + [OnOff.cluster_id, LevelControl.cluster_id, Basic.cluster_id], [], DeviceType.ON_OFF_LIGHT, zha_gateway, diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index d16cafb7df8..c348ef0d0a7 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -86,6 +86,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): A dict containing relevant device info for testing is returned. It contains the entity id, zigpy device, and the zigbee cluster for the sensor. """ + from zigpy.zcl.clusters.general import Basic device_infos = {} counter = 0 for cluster_id in cluster_ids: @@ -93,7 +94,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): device_infos[cluster_id] = {"zigpy_device": None} device_infos[cluster_id]["zigpy_device"] = await \ async_init_zigpy_device( - hass, [cluster_id], [], None, zha_gateway, + hass, [cluster_id, Basic.cluster_id], [], None, zha_gateway, ieee="{}0:15:8d:00:02:32:4f:32".format(counter), manufacturer="Fake{}".format(cluster_id), model="FakeModel{}".format(cluster_id)) diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 32c8ee64e67..1fc21e34cd8 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -14,12 +14,12 @@ OFF = 0 async def test_switch(hass, config_entry, zha_gateway): """Test zha switch platform.""" - from zigpy.zcl.clusters.general import OnOff + from zigpy.zcl.clusters.general import OnOff, Basic from zigpy.zcl.foundation import Status # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [OnOff.cluster_id], [], None, zha_gateway) + hass, [OnOff.cluster_id, Basic.cluster_id], [], None, zha_gateway) # load up switch domain await hass.config_entries.async_forward_entry_setup( From 9ce8f4737d4fc02f2f4a87cb683edd5e4082dc31 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 18 Feb 2019 17:43:22 +0100 Subject: [PATCH 062/253] deCONZ thermostat support (#20586) * Add support for thermostats in deCONZ by adding Climate platform --- homeassistant/components/deconz/__init__.py | 5 +- .../components/deconz/binary_sensor.py | 5 +- homeassistant/components/deconz/climate.py | 111 +++++++++++ homeassistant/components/deconz/const.py | 16 +- homeassistant/components/deconz/cover.py | 7 +- homeassistant/components/deconz/gateway.py | 8 +- homeassistant/components/deconz/light.py | 8 +- homeassistant/components/deconz/scene.py | 5 +- homeassistant/components/deconz/sensor.py | 8 +- homeassistant/components/deconz/switch.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_climate.py | 181 ++++++++++++++++++ tests/components/deconz/test_gateway.py | 14 +- tests/components/deconz/test_light.py | 5 +- tests/components/deconz/test_scene.py | 1 + 16 files changed, 347 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/deconz/climate.py create mode 100644 tests/components/deconz/test_climate.py diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 8015324be13..e640e9ba896 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -12,10 +12,7 @@ from .config_flow import configured_hosts from .const import DEFAULT_PORT, DOMAIN, _LOGGER from .gateway import DeconzGateway -REQUIREMENTS = ['pydeconz==47'] - -SUPPORTED_PLATFORMS = ['binary_sensor', 'cover', - 'light', 'scene', 'sensor', 'switch'] +REQUIREMENTS = ['pydeconz==52'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 77d01c5c40b..cb68b842f4a 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -5,7 +5,8 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( - ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN) + ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN, + NEW_SENSOR) from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -34,7 +35,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) + async_dispatcher_connect(hass, NEW_SENSOR, async_add_sensor)) async_add_sensor(gateway.api.sensors.values()) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py new file mode 100644 index 00000000000..1f39b8705c7 --- /dev/null +++ b/homeassistant/components/deconz/climate.py @@ -0,0 +1,111 @@ +"""Support for deCONZ climate devices.""" +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, TEMP_CELSIUS) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import ( + ATTR_OFFSET, ATTR_VALVE, CONF_ALLOW_CLIP_SENSOR, + DOMAIN as DECONZ_DOMAIN, NEW_SENSOR) +from .deconz_device import DeconzDevice + +DEPENDENCIES = ['deconz'] + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the deCONZ climate devices. + + Thermostats are based on the same device class as sensors in deCONZ. + """ + gateway = hass.data[DECONZ_DOMAIN] + + @callback + def async_add_climate(sensors): + """Add climate devices from deCONZ.""" + from pydeconz.sensor import THERMOSTAT + entities = [] + allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) + for sensor in sensors: + if sensor.type in THERMOSTAT and \ + not (not allow_clip_sensor and sensor.type.startswith('CLIP')): + entities.append(DeconzThermostat(sensor, gateway)) + async_add_entities(entities, True) + + gateway.listeners.append( + async_dispatcher_connect(hass, NEW_SENSOR, async_add_climate)) + + async_add_climate(gateway.api.sensors.values()) + + +class DeconzThermostat(DeconzDevice, ClimateDevice): + """Representation of a deCONZ thermostat.""" + + def __init__(self, device, gateway): + """Set up thermostat device.""" + super().__init__(device, gateway) + + self._features = SUPPORT_ON_OFF + self._features |= SUPPORT_TARGET_TEMPERATURE + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._features + + @property + def is_on(self): + """Return true if on.""" + return self._device.on + + async def async_turn_on(self): + """Turn on switch.""" + data = {'mode': 'auto'} + await self._device.async_set_config(data) + + async def async_turn_off(self): + """Turn off switch.""" + data = {'mode': 'off'} + await self._device.async_set_config(data) + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._device.temperature + + @property + def target_temperature(self): + """Return the target temperature.""" + return self._device.heatsetpoint + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + data = {} + + if ATTR_TEMPERATURE in kwargs: + data['heatsetpoint'] = kwargs[ATTR_TEMPERATURE] * 100 + + await self._device.async_set_config(data) + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the state attributes of the thermostat.""" + attr = {} + + if self._device.battery: + attr[ATTR_BATTERY_LEVEL] = self._device.battery + + if self._device.offset: + attr[ATTR_OFFSET] = self._device.offset + + if self._device.valve is not None: + attr[ATTR_VALVE] = self._device.valve + + return attr diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index b08f3d71824..bf0799d1fa2 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -10,13 +10,27 @@ DEFAULT_PORT = 80 CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' -SUPPORTED_PLATFORMS = ['binary_sensor', 'cover', +SUPPORTED_PLATFORMS = ['binary_sensor', 'climate', 'cover', 'light', 'scene', 'sensor', 'switch'] DECONZ_REACHABLE = 'deconz_reachable' +NEW_GROUP = 'deconz_new_group' +NEW_LIGHT = 'deconz_new_light' +NEW_SCENE = 'deconz_new_scene' +NEW_SENSOR = 'deconz_new_sensor' + +NEW_DEVICE = { + 'group': NEW_GROUP, + 'light': NEW_LIGHT, + 'scene': NEW_SCENE, + 'sensor': NEW_SENSOR +} + ATTR_DARK = 'dark' +ATTR_OFFSET = 'offset' ATTR_ON = 'on' +ATTR_VALVE = 'valve' DAMPERS = ["Level controllable output"] WINDOW_COVERS = ["Window covering device"] diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 48f06a894bb..fda4fe4309c 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -5,7 +5,8 @@ from homeassistant.components.cover import ( from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import COVER_TYPES, DAMPERS, DOMAIN as DECONZ_DOMAIN, WINDOW_COVERS +from .const import ( + COVER_TYPES, DAMPERS, DOMAIN as DECONZ_DOMAIN, NEW_LIGHT, WINDOW_COVERS) from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -39,7 +40,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_light', async_add_cover)) + async_dispatcher_connect(hass, NEW_LIGHT, async_add_cover)) async_add_cover(gateway.api.lights.values()) @@ -48,7 +49,7 @@ class DeconzCover(DeconzDevice, CoverDevice): """Representation of a deCONZ cover.""" def __init__(self, device, gateway): - """Set up cover and add update callback to get data from websocket.""" + """Set up cover device.""" super().__init__(device, gateway) self._features = SUPPORT_OPEN diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 2b53a0998d0..829485e1e92 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -8,7 +8,8 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.util import slugify from .const import ( - _LOGGER, DECONZ_REACHABLE, CONF_ALLOW_CLIP_SENSOR, SUPPORTED_PLATFORMS) + _LOGGER, DECONZ_REACHABLE, CONF_ALLOW_CLIP_SENSOR, NEW_DEVICE, NEW_SENSOR, + SUPPORTED_PLATFORMS) class DeconzGateway: @@ -44,7 +45,7 @@ class DeconzGateway: self.listeners.append( async_dispatcher_connect( - hass, 'deconz_new_sensor', self.async_add_remote)) + hass, NEW_SENSOR, self.async_add_remote)) self.async_add_remote(self.api.sensors.values()) @@ -64,8 +65,7 @@ class DeconzGateway: """Handle event of new device creation in deCONZ.""" if not isinstance(device, list): device = [device] - async_dispatcher_send( - self.hass, 'deconz_new_{}'.format(device_type), device) + async_dispatcher_send(self.hass, NEW_DEVICE[device_type], device) @callback def async_add_remote(self, sensors): diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 50e22c84d6f..3b63da8d9f8 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -9,8 +9,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util from .const import ( - CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DECONZ_DOMAIN, COVER_TYPES, - SWITCH_TYPES) + CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DECONZ_DOMAIN, COVER_TYPES, NEW_GROUP, + NEW_LIGHT, SWITCH_TYPES) from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -36,7 +36,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_light', async_add_light)) + async_dispatcher_connect(hass, NEW_LIGHT, async_add_light)) @callback def async_add_group(groups): @@ -49,7 +49,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_group', async_add_group)) + async_dispatcher_connect(hass, NEW_GROUP, async_add_group)) async_add_light(gateway.api.lights.values()) async_add_group(gateway.api.groups.values()) diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index d3a6df810ba..22b4c47f2ab 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -1,9 +1,10 @@ """Support for deCONZ scenes.""" -from homeassistant.components.deconz import DOMAIN as DECONZ_DOMAIN from homeassistant.components.scene import Scene from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .const import DOMAIN as DECONZ_DOMAIN, NEW_SCENE + DEPENDENCIES = ['deconz'] @@ -25,7 +26,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(DeconzScene(scene, gateway)) async_add_entities(entities) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_scene', async_add_scene)) + async_dispatcher_connect(hass, NEW_SCENE, async_add_scene)) async_add_scene(gateway.api.scenes.values()) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 3083f0c6732..e6b033906e7 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -6,7 +6,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify from .const import ( - ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN) + ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN, + NEW_SENSOR) from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -29,7 +30,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): @callback def async_add_sensor(sensors): """Add sensors from deCONZ.""" - from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE + from pydeconz.sensor import ( + DECONZ_SENSOR, SWITCH as DECONZ_REMOTE) entities = [] allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) for sensor in sensors: @@ -43,7 +45,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) + async_dispatcher_connect(hass, NEW_SENSOR, async_add_sensor)) async_add_sensor(gateway.api.sensors.values()) diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index c48c7205e01..56d37d504cb 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -3,7 +3,7 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS, SIRENS +from .const import DOMAIN as DECONZ_DOMAIN, NEW_LIGHT, POWER_PLUGS, SIRENS from .deconz_device import DeconzDevice DEPENDENCIES = ['deconz'] @@ -34,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) gateway.listeners.append( - async_dispatcher_connect(hass, 'deconz_new_light', async_add_switch)) + async_dispatcher_connect(hass, NEW_LIGHT, async_add_switch)) async_add_switch(gateway.api.lights.values()) diff --git a/requirements_all.txt b/requirements_all.txt index 90e6e2b9c34..22124c3910a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -983,7 +983,7 @@ pydaikin==0.9 pydanfossair==0.0.7 # homeassistant.components.deconz -pydeconz==47 +pydeconz==52 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df4112b71a0..109cf460ab3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -184,7 +184,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==47 +pydeconz==52 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py new file mode 100644 index 00000000000..13083594c8a --- /dev/null +++ b/tests/components/deconz/test_climate.py @@ -0,0 +1,181 @@ +"""deCONZ climate platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.setup import async_setup_component + +import homeassistant.components.climate as climate + +from tests.common import mock_coro + + +SENSOR = { + "1": { + "id": "Climate 1 id", + "name": "Climate 1 name", + "type": "ZHAThermostat", + "state": {"on": True, "temperature": 2260}, + "config": {"battery": 100, "heatsetpoint": 2200, "mode": "auto", + "offset": 10, "reachable": True, "valve": 30}, + "uniqueid": "00:00:00:00:00:00:00:00-00" + }, + "2": { + "id": "Sensor 2 id", + "name": "Sensor 2 name", + "type": "ZHAPresence", + "state": {"presence": False}, + "config": {} + } +} + +ENTRY_CONFIG = { + deconz.const.CONF_ALLOW_CLIP_SENSOR: True, + deconz.const.CONF_ALLOW_DECONZ_GROUPS: True, + deconz.config_flow.CONF_API_KEY: "ABCDEF", + deconz.config_flow.CONF_BRIDGEID: "0123456789", + deconz.config_flow.CONF_HOST: "1.2.3.4", + deconz.config_flow.CONF_PORT: 80 +} + + +async def setup_gateway(hass, data, allow_clip_sensor=True): + """Load the deCONZ sensor platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + + ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor + + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test', + config_entries.CONN_CLASS_LOCAL_PUSH) + gateway = deconz.DeconzGateway(hass, config_entry) + gateway.api = DeconzSession(loop, session, **config_entry.data) + gateway.api.config = Mock() + hass.data[deconz.DOMAIN] = gateway + + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await gateway.api.async_load_parameters() + + await hass.config_entries.async_forward_entry_setup( + config_entry, 'climate') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_platform_manually_configured(hass): + """Test that we do not discover anything or try to set up a gateway.""" + assert await async_setup_component(hass, climate.DOMAIN, { + 'climate': { + 'platform': deconz.DOMAIN + } + }) is True + assert deconz.DOMAIN not in hass.data + + +async def test_no_sensors(hass): + """Test that no sensors in deconz results in no climate entities.""" + await setup_gateway(hass, {}) + assert not hass.data[deconz.DOMAIN].deconz_ids + assert not hass.states.async_all() + + +async def test_climate_devices(hass): + """Test successful creation of sensor entities.""" + await setup_gateway(hass, {"sensors": SENSOR}) + assert "climate.climate_1_name" in hass.data[deconz.DOMAIN].deconz_ids + assert "sensor.sensor_2_name" not in hass.data[deconz.DOMAIN].deconz_ids + assert len(hass.states.async_all()) == 1 + + hass.data[deconz.DOMAIN].api.sensors['1'].async_update( + {'state': {'on': False}}) + + await hass.services.async_call( + 'climate', 'turn_on', {'entity_id': 'climate.climate_1_name'}, + blocking=True + ) + hass.data[deconz.DOMAIN].api.session.put.assert_called_with( + 'http://1.2.3.4:80/api/ABCDEF/sensors/1/config', + data='{"mode": "auto"}' + ) + + await hass.services.async_call( + 'climate', 'turn_off', {'entity_id': 'climate.climate_1_name'}, + blocking=True + ) + hass.data[deconz.DOMAIN].api.session.put.assert_called_with( + 'http://1.2.3.4:80/api/ABCDEF/sensors/1/config', + data='{"mode": "off"}' + ) + + await hass.services.async_call( + 'climate', 'set_temperature', + {'entity_id': 'climate.climate_1_name', 'temperature': 20}, + blocking=True + ) + hass.data[deconz.DOMAIN].api.session.put.assert_called_with( + 'http://1.2.3.4:80/api/ABCDEF/sensors/1/config', + data='{"heatsetpoint": 2000.0}' + ) + + assert len(hass.data[deconz.DOMAIN].api.session.put.mock_calls) == 3 + + +async def test_verify_state_update(hass): + """Test that state update properly.""" + await setup_gateway(hass, {"sensors": SENSOR}) + assert "climate.climate_1_name" in hass.data[deconz.DOMAIN].deconz_ids + + thermostat = hass.states.get('climate.climate_1_name') + assert thermostat.state == 'on' + + state_update = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "config": {"on": False} + } + hass.data[deconz.DOMAIN].api.async_event_handler(state_update) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + thermostat = hass.states.get('climate.climate_1_name') + assert thermostat.state == 'off' + + +async def test_add_new_climate_device(hass): + """Test successful creation of climate entities.""" + await setup_gateway(hass, {}) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'ZHAThermostat' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert "climate.name" in hass.data[deconz.DOMAIN].deconz_ids + + +async def test_do_not_allow_clipsensor(hass): + """Test that clip sensors can be ignored.""" + await setup_gateway(hass, {}, allow_clip_sensor=False) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'CLIPThermostat' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DOMAIN].deconz_ids) == 0 + + +async def test_unload_sensor(hass): + """Test that it works to unload sensor entities.""" + await setup_gateway(hass, {"sensors": SENSOR}) + + await hass.data[deconz.DOMAIN].async_reset() + + assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index dbc45c955b5..d73f225b2ac 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -35,18 +35,20 @@ async def test_gateway_setup(): assert await deconz_gateway.async_setup() is True assert deconz_gateway.api is api - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 6 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 7 assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \ (entry, 'binary_sensor') assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == \ - (entry, 'cover') + (entry, 'climate') assert hass.config_entries.async_forward_entry_setup.mock_calls[2][1] == \ - (entry, 'light') + (entry, 'cover') assert hass.config_entries.async_forward_entry_setup.mock_calls[3][1] == \ - (entry, 'scene') + (entry, 'light') assert hass.config_entries.async_forward_entry_setup.mock_calls[4][1] == \ - (entry, 'sensor') + (entry, 'scene') assert hass.config_entries.async_forward_entry_setup.mock_calls[5][1] == \ + (entry, 'sensor') + assert hass.config_entries.async_forward_entry_setup.mock_calls[6][1] == \ (entry, 'switch') assert len(api.start.mock_calls) == 1 @@ -150,7 +152,7 @@ async def test_reset_after_successful_setup(): mock_coro(True) assert await deconz_gateway.async_reset() is True - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 6 + assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 7 assert len(listener.mock_calls) == 1 assert len(deconz_gateway.listeners) == 0 diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 081fd61ec4e..49c3f280d8a 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -41,14 +41,15 @@ GROUP = { "lights": [ "1", "2" - ] + ], }, "2": { "id": "Group 2 id", "name": "Group 2 name", "state": {}, "action": {}, - "scenes": [] + "scenes": [], + "lights": [], }, } diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index 788c6dc1c3e..963f1064b35 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -20,6 +20,7 @@ GROUP = { "id": "1", "name": "Scene 1" }], + "lights": [], } } From 600070af3ab753d00252ec8fe4be070472c2f088 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 18 Feb 2019 13:04:04 -0800 Subject: [PATCH 063/253] Make sure that device trackers is always a list during creation (#21193) --- homeassistant/components/person/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 6fb7d42e0ee..3e0b00f7442 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -176,7 +176,7 @@ class PersonManager: CONF_ID: uuid.uuid4().hex, CONF_NAME: name, CONF_USER_ID: user_id, - CONF_DEVICE_TRACKERS: device_trackers, + CONF_DEVICE_TRACKERS: device_trackers or [], } self.storage_data[person[CONF_ID]] = person self._async_schedule_save() From 463c4ae5c9eff894bb0a8152abd712d2766129ef Mon Sep 17 00:00:00 2001 From: sjabby Date: Mon, 18 Feb 2019 22:05:46 +0100 Subject: [PATCH 064/253] Fix for #19072 (#21175) * Fix for #19072 PR #19072 introduced the custom_effect feature but it didnt make it optional as the documentation states. This causes error on startup and the component does not work. ``` Error while setting up platform flux_led Traceback (most recent call last): File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/helpers/entity_platform.py", line 128, in _async_setup_platform SLOW_SETUP_MAX_WAIT, loop=hass.loop) File "/usr/lib/python3.5/asyncio/tasks.py", line 400, in wait_for return fut.result() File "/usr/lib/python3.5/asyncio/futures.py", line 293, in result raise self._exception File "/usr/lib/python3.5/concurrent/futures/thread.py", line 55, in run result = self.fn(*self.args, **self.kwargs) File "/srv/homeassistant/lib/python3.5/site-packages/homeassistant/components/light/flux_led.py", line 135, in setup_platform device[CONF_CUSTOM_EFFECT] = device_config[CONF_CUSTOM_EFFECT] KeyError: 'custom_effect' ``` Changing this line to make the custom_effect optional as the original intention. * Update flux_led.py --- homeassistant/components/light/flux_led.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 088fc871fc1..5ecf3f55e10 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -132,7 +132,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device['ipaddr'] = ipaddr device[CONF_PROTOCOL] = device_config.get(CONF_PROTOCOL) device[ATTR_MODE] = device_config[ATTR_MODE] - device[CONF_CUSTOM_EFFECT] = device_config[CONF_CUSTOM_EFFECT] + device[CONF_CUSTOM_EFFECT] = device_config.get(CONF_CUSTOM_EFFECT) light = FluxLight(device) lights.append(light) light_ips.append(ipaddr) From d2fea76fd7e3466bd80498acd3669604ede31387 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 18 Feb 2019 13:07:44 -0800 Subject: [PATCH 065/253] Add context to service call event (#21181) --- homeassistant/core.py | 2 +- tests/test_core.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 5cd23e9f9a2..c181ad453f3 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1117,7 +1117,7 @@ class ServiceRegistry: ATTR_DOMAIN: domain.lower(), ATTR_SERVICE: service.lower(), ATTR_SERVICE_DATA: service_data, - }) + }, context=context) if not blocking: self._hass.async_create_task( diff --git a/tests/test_core.py b/tests/test_core.py index 3cb5b87b4bb..818c6e4c087 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1013,6 +1013,7 @@ def test_track_task_functions(loop): async def test_service_executed_with_subservices(hass): """Test we block correctly till all services done.""" calls = async_mock_service(hass, 'test', 'inner') + context = ha.Context() async def handle_outer(call): """Handle outer service call.""" @@ -1026,11 +1027,13 @@ async def test_service_executed_with_subservices(hass): hass.services.async_register('test', 'outer', handle_outer) - await hass.services.async_call('test', 'outer', blocking=True) + await hass.services.async_call('test', 'outer', blocking=True, + context=context) assert len(calls) == 4 assert [call.service for call in calls] == [ 'outer', 'inner', 'inner', 'outer'] + assert all(call.context is context for call in calls) async def test_service_call_event_contains_original_data(hass): @@ -1047,11 +1050,14 @@ async def test_service_call_event_contains_original_data(hass): 'number': vol.Coerce(int) })) + context = ha.Context() await hass.services.async_call('test', 'service', { 'number': '23' - }, blocking=True) + }, blocking=True, context=context) await hass.async_block_till_done() assert len(events) == 1 assert events[0].data['service_data']['number'] == '23' + assert events[0].context is context assert len(calls) == 1 assert calls[0].data['number'] == 23 + assert calls[0].context is context From d1ebe2cbac1298ba38d8525c49d59e1ee8ae2404 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 18 Feb 2019 13:25:43 -0800 Subject: [PATCH 066/253] Updated frontend to 20190218.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 3db63e65a6d..3b1d961ebe7 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190216.0'] +REQUIREMENTS = ['home-assistant-frontend==20190218.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 22124c3910a..aeeb8405458 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190216.0 +home-assistant-frontend==20190218.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 109cf460ab3..9f43a8dc552 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -116,7 +116,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190216.0 +home-assistant-frontend==20190218.0 # homeassistant.components.homekit_controller homekit==0.12.2 From bdea22219631b52d82df57a0eb8e4ee5358b87c4 Mon Sep 17 00:00:00 2001 From: OleksandrBerchenko Date: Tue, 19 Feb 2019 07:01:26 +0200 Subject: [PATCH 067/253] Expose effect_list attribute for turned off lights (#20750) --- homeassistant/components/light/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 816f93b5881..93d7a67c6f0 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -440,6 +440,9 @@ class Light(ToggleEntity): data[ATTR_MIN_MIREDS] = self.min_mireds data[ATTR_MAX_MIREDS] = self.max_mireds + if supported_features & SUPPORT_EFFECT: + data[ATTR_EFFECT_LIST] = self.effect_list + if self.is_on: if supported_features & SUPPORT_BRIGHTNESS: data[ATTR_BRIGHTNESS] = self.brightness @@ -461,7 +464,6 @@ class Light(ToggleEntity): data[ATTR_WHITE_VALUE] = self.white_value if supported_features & SUPPORT_EFFECT: - data[ATTR_EFFECT_LIST] = self.effect_list data[ATTR_EFFECT] = self.effect return {key: val for key, val in data.items() if val is not None} From bc46e48d234384b00275f58050e10163d1abb9b1 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 19 Feb 2019 06:11:56 +0100 Subject: [PATCH 068/253] Upgrade aioimaplib for Python 3.7 compatibility (#21197) --- homeassistant/components/sensor/imap.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/imap.py b/homeassistant/components/sensor/imap.py index b8d363417c2..571d05e78e9 100644 --- a/homeassistant/components/sensor/imap.py +++ b/homeassistant/components/sensor/imap.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aioimaplib==0.7.13'] +REQUIREMENTS = ['aioimaplib==0.7.15'] CONF_SERVER = 'server' CONF_FOLDER = 'folder' diff --git a/requirements_all.txt b/requirements_all.txt index aeeb8405458..455d4216037 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -124,7 +124,7 @@ aiohue==1.9.0 aioiliad==0.1.1 # homeassistant.components.sensor.imap -aioimaplib==0.7.13 +aioimaplib==0.7.15 # homeassistant.components.lifx aiolifx==0.6.7 From 921efbdfef1e6d419b654a28f4219084c1bc5847 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 19 Feb 2019 06:31:42 +0100 Subject: [PATCH 069/253] Philips Hue: Add bridge update prompt (#21119) * Add a prompt if bridge update is available. * Change logger warning for light update The self.light.swupdatestate only checks for updates of that specific light, it does not check for updates of the bridge. Theirfore the warning message schould be updated. * add space * fix tests * rename to swupdate2_bridge_state * update aiohue to v1.9.1 * update aiohue to v1.9.1 * update aiohue to v1.9.1 --- homeassistant/components/hue/__init__.py | 14 ++++++++++---- homeassistant/components/hue/light.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/test_init.py | 6 ++---- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 104965b7b8f..8f5c27f6516 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -14,7 +14,7 @@ from .bridge import HueBridge # Loading the config flow file will register the flow from .config_flow import configured_hosts -REQUIREMENTS = ['aiohue==1.9.0'] +REQUIREMENTS = ['aiohue==1.9.1'] _LOGGER = logging.getLogger(__name__) @@ -121,11 +121,17 @@ async def async_setup_entry(hass, entry): }, manufacturer='Signify', name=config.name, - # Not yet exposed as properties in aiohue - model=config.raw['modelid'], - sw_version=config.raw['swversion'], + model=config.modelid, + sw_version=config.swversion, ) + if config.swupdate2_bridge_state == "readytoinstall": + err = ( + "Please check for software updates of the bridge " + "in the Philips Hue App." + ) + _LOGGER.warning(err) + return True diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 7518a5a381f..0725c86bd95 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -227,8 +227,8 @@ class HueLight(Light): _LOGGER.debug("Color gamut of %s: %s", self.name, str(self.gamut)) if self.light.swupdatestate == "readytoinstall": err = ( - "Please check for software updates of the bridge " - "and/or the bulb: %s, in the Philips Hue App." + "Please check for software updates of the %s " + "bulb in the Philips Hue App." ) _LOGGER.warning(err, self.name) if self.gamut: diff --git a/requirements_all.txt b/requirements_all.txt index 455d4216037..f4390c43800 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -118,7 +118,7 @@ aioharmony==0.1.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.0 +aiohue==1.9.1 # homeassistant.components.sensor.iliad_italy aioiliad==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f43a8dc552..82a5e4ba8b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -41,7 +41,7 @@ aioautomatic==0.6.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.0 +aiohue==1.9.1 # homeassistant.components.unifi aiounifi==4 diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 6c89995a1a1..cdad8e02d25 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -97,10 +97,8 @@ async def test_config_passed_to_config_entry(hass): mock_bridge.return_value.api.config = Mock( mac='mock-mac', bridgeid='mock-bridgeid', - raw={ - 'modelid': 'mock-modelid', - 'swversion': 'mock-swversion', - } + modelid='mock-modelid', + swversion='mock-swversion' ) # Can't set name via kwargs mock_bridge.return_value.api.config.name = 'mock-name' From baaeaab61da41894d8ce17ca4a8e36b29f9a38ad Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 19 Feb 2019 10:04:14 +0100 Subject: [PATCH 070/253] Upgrade crimereports to 1.0.1 (#21187) --- homeassistant/components/sensor/crimereports.py | 9 ++------- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/crimereports.py b/homeassistant/components/sensor/crimereports.py index 2f1db42a127..13934675517 100644 --- a/homeassistant/components/sensor/crimereports.py +++ b/homeassistant/components/sensor/crimereports.py @@ -1,9 +1,4 @@ -""" -Sensor for Crime Reports. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.crimereports/ -""" +"""Sensor for Crime Reports.""" from collections import defaultdict from datetime import timedelta import logging @@ -21,7 +16,7 @@ from homeassistant.util.distance import convert from homeassistant.util.dt import now import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['crimereports==1.0.0'] +REQUIREMENTS = ['crimereports==1.0.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f4390c43800..901a1b2bbeb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -292,7 +292,7 @@ construct==2.9.45 # credstash==1.15.0 # homeassistant.components.sensor.crimereports -crimereports==1.0.0 +crimereports==1.0.1 # homeassistant.components.datadog datadog==0.15.0 From 9d3eaada27f9f8360a79095de864fb3a51cba6d0 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Tue, 19 Feb 2019 10:53:45 +0100 Subject: [PATCH 071/253] Netatmo, address comments from #20755 (#21157) Netatmo component cleanup --- homeassistant/components/netatmo/__init__.py | 29 ++++++++++--------- .../components/netatmo/binary_sensor.py | 2 +- homeassistant/components/netatmo/camera.py | 2 +- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index c496553f057..2e580627543 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,6 +1,5 @@ """Support for the Netatmo devices.""" import logging -import json from datetime import timedelta from urllib.error import HTTPError @@ -18,6 +17,9 @@ DEPENDENCIES = ['webhook'] _LOGGER = logging.getLogger(__name__) +DATA_PERSONS = 'netatmo_persons' +DATA_WEBHOOK_URL = 'netatmo_webhook_url' + CONF_SECRET_KEY = 'secret_key' CONF_WEBHOOKS = 'webhooks' @@ -28,7 +30,6 @@ SERVICE_DROPWEBHOOK = 'dropwebhook' NETATMO_AUTH = None NETATMO_WEBHOOK_URL = None -NETATMO_PERSONS = {} DEFAULT_PERSON = 'Unknown' DEFAULT_DISCOVERY = True @@ -85,7 +86,8 @@ def setup(hass, config): """Set up the Netatmo devices.""" import pyatmo - global NETATMO_AUTH, NETATMO_WEBHOOK_URL + global NETATMO_AUTH + hass.data[DATA_PERSONS] = {} try: NETATMO_AUTH = pyatmo.ClientAuth( config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY], @@ -103,11 +105,12 @@ def setup(hass, config): if config[DOMAIN][CONF_WEBHOOKS]: webhook_id = hass.components.webhook.async_generate_id() - NETATMO_WEBHOOK_URL = hass.components.webhook.async_generate_url( - webhook_id) + hass.data[ + DATA_WEBHOOK_URL] = hass.components.webhook.async_generate_url( + webhook_id) hass.components.webhook.async_register( DOMAIN, 'Netatmo', webhook_id, handle_webhook) - NETATMO_AUTH.addwebhook(NETATMO_WEBHOOK_URL) + NETATMO_AUTH.addwebhook(hass.data[DATA_WEBHOOK_URL]) hass.bus.listen_once( EVENT_HOMEASSISTANT_STOP, dropwebhook) @@ -115,7 +118,7 @@ def setup(hass, config): """Service to (re)add webhooks during runtime.""" url = service.data.get(CONF_URL) if url is None: - url = NETATMO_WEBHOOK_URL + url = hass.data[DATA_WEBHOOK_URL] _LOGGER.info("Adding webhook for URL: %s", url) NETATMO_AUTH.addwebhook(url) @@ -142,9 +145,8 @@ def dropwebhook(hass): async def handle_webhook(hass, webhook_id, request): """Handle webhook callback.""" - body = await request.text() try: - data = json.loads(body) if body else {} + data = await request.json() except ValueError: return None @@ -158,7 +160,7 @@ async def handle_webhook(hass, webhook_id, request): if data.get(ATTR_EVENT_TYPE) == EVENT_PERSON: for person in data[ATTR_PERSONS]: published_data[ATTR_ID] = person.get(ATTR_ID) - published_data[ATTR_NAME] = NETATMO_PERSONS.get( + published_data[ATTR_NAME] = hass.data[DATA_PERSONS].get( published_data[ATTR_ID], DEFAULT_PERSON) published_data[ATTR_IS_KNOWN] = person.get(ATTR_IS_KNOWN) published_data[ATTR_FACE_URL] = person.get(ATTR_FACE_URL) @@ -186,8 +188,9 @@ async def handle_webhook(hass, webhook_id, request): class CameraData: """Get the latest data from Netatmo.""" - def __init__(self, auth, home=None): + def __init__(self, hass, auth, home=None): """Initialize the data object.""" + self._hass = hass self.auth = auth self.camera_data = None self.camera_names = [] @@ -227,9 +230,9 @@ class CameraData: def get_persons(self): """Gather person data for webhooks.""" - global NETATMO_PERSONS for person_id, person_data in self.camera_data.persons.items(): - NETATMO_PERSONS[person_id] = person_data.get(ATTR_PSEUDO) + self._hass.data[DATA_PERSONS][person_id] = person_data.get( + ATTR_PSEUDO) @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 727ed0a68c7..7986010ef64 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -62,7 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): import pyatmo try: - data = CameraData(netatmo.NETATMO_AUTH, home) + data = CameraData(hass, netatmo.NETATMO_AUTH, home) if not data.get_camera_names(): return None except pyatmo.NoDevice: diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index af56dc6e621..57d30d6cbc9 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): verify_ssl = config.get(CONF_VERIFY_SSL, True) import pyatmo try: - data = CameraData(netatmo.NETATMO_AUTH, home) + data = CameraData(hass, netatmo.NETATMO_AUTH, home) for camera_name in data.get_camera_names(): camera_type = data.get_camera_type(camera=camera_name, home=home) if CONF_CAMERAS in config: From 2d2c6cf4a191679ff1cbe19228e96a09c3bd2dd8 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 19 Feb 2019 14:09:06 +0100 Subject: [PATCH 072/253] Use constants from const.py (#21068) * Use constants from const.py * Fix lint issues --- homeassistant/components/insteon/__init__.py | 17 +++----- .../components/insteon/binary_sensor.py | 36 +++++++--------- homeassistant/components/insteon/cover.py | 17 +++----- homeassistant/components/insteon/fan.py | 42 ++++++++----------- homeassistant/components/insteon/light.py | 11 ++--- homeassistant/components/insteon/sensor.py | 11 ++--- homeassistant/components/insteon/switch.py | 13 ++---- homeassistant/components/knx/binary_sensor.py | 13 ++---- homeassistant/components/knx/climate.py | 33 +++++++-------- homeassistant/components/knx/light.py | 10 ++--- homeassistant/components/knx/notify.py | 9 ++-- homeassistant/components/knx/scene.py | 3 +- homeassistant/components/knx/sensor.py | 5 +-- homeassistant/components/knx/switch.py | 3 +- .../components/light/rpi_gpio_pwm.py | 16 +++---- .../components/raspihats/__init__.py | 1 - .../components/raspihats/binary_sensor.py | 6 +-- homeassistant/components/raspihats/switch.py | 7 ++-- homeassistant/components/sensor/etherscan.py | 12 ++---- homeassistant/components/sensor/ripple.py | 12 ++---- homeassistant/components/sensor/sochain.py | 16 +++---- 21 files changed, 105 insertions(+), 188 deletions(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index e82e47dc5f4..a462ac0f63e 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -1,22 +1,16 @@ -""" -Support for INSTEON Modems (PLM and Hub). - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/insteon/ -""" +"""Support for INSTEON Modems (PLM and Hub).""" import collections import logging from typing import Dict import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import (CONF_PORT, EVENT_HOMEASSISTANT_STOP, - CONF_PLATFORM, - CONF_ENTITY_ID, - CONF_HOST) +from homeassistant.const import ( + CONF_ADDRESS, CONF_ENTITY_ID, CONF_HOST, CONF_PLATFORM, CONF_PORT, + EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity REQUIREMENTS = ['insteonplm==0.15.2'] @@ -31,7 +25,6 @@ CONF_HUB_PASSWORD = 'password' CONF_HUB_VERSION = 'hub_version' CONF_OVERRIDE = 'device_override' CONF_PLM_HUB_MSG = 'Must configure either a PLM port or a Hub host' -CONF_ADDRESS = 'address' CONF_CAT = 'cat' CONF_SUBCAT = 'subcat' CONF_FIRMWARE = 'firmware' diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py index 5b0a291e92b..06eddb9a004 100644 --- a/homeassistant/components/insteon/binary_sensor.py +++ b/homeassistant/components/insteon/binary_sensor.py @@ -1,29 +1,26 @@ -""" -Support for INSTEON dimmers via PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.insteon/ -""" +"""Support for INSTEON dimmers via PowerLinc Modem.""" import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.insteon import InsteonEntity -DEPENDENCIES = ['insteon'] - _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES = {'openClosedSensor': 'opening', - 'ioLincSensor': 'opening', - 'motionSensor': 'motion', - 'doorSensor': 'door', - 'wetLeakSensor': 'moisture', - 'lightSensor': 'light', - 'batterySensor': 'battery'} +DEPENDENCIES = ['insteon'] + +SENSOR_TYPES = { + 'openClosedSensor': 'opening', + 'ioLincSensor': 'opening', + 'motionSensor': 'motion', + 'doorSensor': 'door', + 'wetLeakSensor': 'moisture', + 'lightSensor': 'light', + 'batterySensor': 'battery', +} -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') @@ -32,7 +29,7 @@ async def async_setup_platform(hass, config, async_add_entities, state_key = discovery_info['state_key'] name = device.states[state_key].name if name != 'dryLeakSensor': - _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', + _LOGGER.debug("Adding device %s entity %s to Binary Sensor platform", device.address.hex, device.states[state_key].name) new_entity = InsteonBinarySensor(device, state_key) @@ -58,8 +55,7 @@ class InsteonBinarySensor(InsteonEntity, BinarySensorDevice): """Return the boolean response if the node is on.""" on_val = bool(self._insteon_device_state.value) - if self._insteon_device_state.name in ['lightSensor', - 'ioLincSensor']: + if self._insteon_device_state.name in ['lightSensor', 'ioLincSensor']: return not on_val return on_val diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py index f0cf93c13e9..7de2e872489 100644 --- a/homeassistant/components/insteon/cover.py +++ b/homeassistant/components/insteon/cover.py @@ -1,16 +1,11 @@ -""" -Support for Insteon covers via PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/cover.insteon/ -""" +"""Support for Insteon covers via PowerLinc Modem.""" import logging import math +from homeassistant.components.cover import ( + ATTR_POSITION, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + CoverDevice) from homeassistant.components.insteon import InsteonEntity -from homeassistant.components.cover import (CoverDevice, ATTR_POSITION, - SUPPORT_OPEN, SUPPORT_CLOSE, - SUPPORT_SET_POSITION) _LOGGER = logging.getLogger(__name__) @@ -18,8 +13,8 @@ DEPENDENCIES = ['insteon'] SUPPORTED_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Insteon platform.""" if not discovery_info: return diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index 604063a9aa3..2b6097a4ba2 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -1,34 +1,28 @@ -""" -Support for INSTEON fans via PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/fan.insteon/ -""" +"""Support for INSTEON fans via PowerLinc Modem.""" import logging -from homeassistant.components.fan import (SPEED_OFF, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, - FanEntity, - SUPPORT_SET_SPEED) -from homeassistant.const import STATE_OFF +from homeassistant.components.fan import ( + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED, + FanEntity) from homeassistant.components.insteon import InsteonEntity - -DEPENDENCIES = ['insteon'] - -SPEED_TO_HEX = {SPEED_OFF: 0x00, - SPEED_LOW: 0x3f, - SPEED_MEDIUM: 0xbe, - SPEED_HIGH: 0xff} - -FAN_SPEEDS = [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] +from homeassistant.const import STATE_OFF _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['insteon'] -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +SPEED_TO_HEX = { + SPEED_OFF: 0x00, + SPEED_LOW: 0x3f, + SPEED_MEDIUM: 0xbe, + SPEED_HIGH: 0xff, +} + +FAN_SPEEDS = [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index 4829ce631a6..e8ffc226716 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -1,9 +1,4 @@ -""" -Support for Insteon lights via PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/light.insteon/ -""" +"""Support for Insteon lights via PowerLinc Modem.""" import logging from homeassistant.components.insteon import InsteonEntity @@ -17,8 +12,8 @@ DEPENDENCIES = ['insteon'] MAX_BRIGHTNESS = 255 -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Insteon component.""" insteon_modem = hass.data['insteon'].get('modem') diff --git a/homeassistant/components/insteon/sensor.py b/homeassistant/components/insteon/sensor.py index 7854967395b..d895d972027 100644 --- a/homeassistant/components/insteon/sensor.py +++ b/homeassistant/components/insteon/sensor.py @@ -1,9 +1,4 @@ -""" -Support for INSTEON dimmers via PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sensor.insteon/ -""" +"""Support for INSTEON dimmers via PowerLinc Modem.""" import logging from homeassistant.components.insteon import InsteonEntity @@ -14,8 +9,8 @@ DEPENDENCIES = ['insteon'] _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py index 454b3ef39cb..2a6b97a39d1 100644 --- a/homeassistant/components/insteon/switch.py +++ b/homeassistant/components/insteon/switch.py @@ -1,9 +1,4 @@ -""" -Support for INSTEON dimmers via PowerLinc Modem. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/switch.insteon/ -""" +"""Support for INSTEON dimmers via PowerLinc Modem.""" import logging from homeassistant.components.insteon import InsteonEntity @@ -14,8 +9,8 @@ DEPENDENCIES = ['insteon'] _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') @@ -25,7 +20,7 @@ async def async_setup_platform(hass, config, async_add_entities, state_name = device.states[state_key].name - _LOGGER.debug('Adding device %s entity %s to Switch platform', + _LOGGER.debug("Adding device %s entity %s to Switch platform", device.address.hex, device.states[state_key].name) new_entity = None diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index ca7037fe81d..c84e5820f04 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -5,12 +5,10 @@ from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorDevice) from homeassistant.components.knx import ( ATTR_DISCOVER_DEVICES, DATA_KNX, KNXAutomation) -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -CONF_ADDRESS = 'address' -CONF_DEVICE_CLASS = 'device_class' CONF_SIGNIFICANT_BIT = 'significant_bit' CONF_DEFAULT_SIGNIFICANT_BIT = 1 CONF_AUTOMATION = 'automation' @@ -32,10 +30,7 @@ AUTOMATION_SCHEMA = vol.Schema({ vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, }) -AUTOMATIONS_SCHEMA = vol.All( - cv.ensure_list, - [AUTOMATION_SCHEMA] -) +AUTOMATIONS_SCHEMA = vol.All(cv.ensure_list, [AUTOMATION_SCHEMA]) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ADDRESS): cv.string, @@ -48,8 +43,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up binary sensor(s) for KNX platform.""" if discovery_info is not None: async_add_entities_discovery(hass, discovery_info, async_add_entities) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 7e172287d4d..96b9f2ea91f 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -1,17 +1,14 @@ """Support for KNX/IP climate devices.""" import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA -from homeassistant.components.climate.const import ( - SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, STATE_HEAT, - STATE_IDLE, STATE_MANUAL, STATE_DRY, - STATE_FAN_ONLY, STATE_ECO) -from homeassistant.const import ( - ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS) -from homeassistant.core import callback -from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate.const import ( + STATE_DRY, STATE_ECO, STATE_FAN_ONLY, STATE_HEAT, STATE_IDLE, STATE_MANUAL, + SUPPORT_ON_OFF, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX +from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, TEMP_CELSIUS +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv CONF_SETPOINT_SHIFT_ADDRESS = 'setpoint_shift_address' CONF_SETPOINT_SHIFT_STATE_ADDRESS = 'setpoint_shift_state_address' @@ -81,15 +78,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string, vol.Optional(CONF_ON_OFF_ADDRESS): cv.string, vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string, - vol.Optional(CONF_OPERATION_MODES): vol.All(cv.ensure_list, - [vol.In(OPERATION_MODES)]), + vol.Optional(CONF_OPERATION_MODES): + vol.All(cv.ensure_list, [vol.In(OPERATION_MODES)]), vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up climate(s) for KNX platform.""" if discovery_info is not None: async_add_entities_discovery(hass, discovery_info, async_add_entities) @@ -148,10 +145,8 @@ def async_add_entities_config(hass, config, async_add_entities): setpoint_shift_step=config.get(CONF_SETPOINT_SHIFT_STEP), setpoint_shift_max=config.get(CONF_SETPOINT_SHIFT_MAX), setpoint_shift_min=config.get(CONF_SETPOINT_SHIFT_MIN), - group_address_on_off=config.get( - CONF_ON_OFF_ADDRESS), - group_address_on_off_state=config.get( - CONF_ON_OFF_STATE_ADDRESS), + group_address_on_off=config.get(CONF_ON_OFF_ADDRESS), + group_address_on_off_state=config.get(CONF_ON_OFF_STATE_ADDRESS), min_temp=config.get(CONF_MIN_TEMP), max_temp=config.get(CONF_MAX_TEMP), mode=climate_mode) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index f2a6f15e08b..baba7edd21a 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -3,19 +3,15 @@ from enum import Enum import voluptuous as vol +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - Light) -from homeassistant.const import CONF_NAME + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX - - -CONF_ADDRESS = 'address' CONF_STATE_ADDRESS = 'state_address' CONF_BRIGHTNESS_ADDRESS = 'brightness_address' CONF_BRIGHTNESS_STATE_ADDRESS = 'brightness_state_address' diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index 2488114aa41..1e1d7f185f0 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -1,14 +1,13 @@ """Support for KNX/IP notification services.""" import voluptuous as vol -from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES -from homeassistant.components.notify import PLATFORM_SCHEMA, \ - BaseNotificationService -from homeassistant.const import CONF_NAME +from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX +from homeassistant.components.notify import ( + PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -CONF_ADDRESS = 'address' DEFAULT_NAME = 'KNX Notify' DEPENDENCIES = ['knx'] diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index 008e81508b9..b1bb2bf3109 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -3,11 +3,10 @@ import voluptuous as vol from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX from homeassistant.components.scene import CONF_PLATFORM, Scene -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -CONF_ADDRESS = 'address' CONF_SCENE_NUMBER = 'scene_number' DEFAULT_NAME = 'KNX SCENE' diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 6a2d8144b1e..abbb61e150d 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -3,14 +3,11 @@ import voluptuous as vol from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -CONF_ADDRESS = 'address' -CONF_TYPE = 'type' - DEFAULT_NAME = 'KNX Sensor' DEPENDENCIES = ['knx'] diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 305234e1eec..cef14fb74dc 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -3,11 +3,10 @@ import voluptuous as vol from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -CONF_ADDRESS = 'address' CONF_STATE_ADDRESS = 'state_address' DEFAULT_NAME = 'KNX Switch' diff --git a/homeassistant/components/light/rpi_gpio_pwm.py b/homeassistant/components/light/rpi_gpio_pwm.py index a3fe0f6b71e..b0b9ef1b763 100644 --- a/homeassistant/components/light/rpi_gpio_pwm.py +++ b/homeassistant/components/light/rpi_gpio_pwm.py @@ -1,14 +1,9 @@ -""" -Support for LED lights that can be controlled using PWM. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.pwm/ -""" +"""Support for LED lights that can be controlled using PWM.""" import logging import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_ON +from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_ON, CONF_ADDRESS from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) @@ -24,7 +19,6 @@ CONF_LEDS = 'leds' CONF_DRIVER = 'driver' CONF_PINS = 'pins' CONF_FREQUENCY = 'frequency' -CONF_ADDRESS = 'address' CONF_DRIVER_GPIO = 'gpio' CONF_DRIVER_PCA9685 = 'pca9685' @@ -46,11 +40,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ { vol.Required(CONF_NAME): cv.string, vol.Required(CONF_DRIVER): vol.In(CONF_DRIVER_TYPES), - vol.Required(CONF_PINS): vol.All(cv.ensure_list, - [cv.positive_int]), + vol.Required(CONF_PINS): + vol.All(cv.ensure_list, [cv.positive_int]), vol.Required(CONF_TYPE): vol.In(CONF_LED_TYPES), vol.Optional(CONF_FREQUENCY): cv.positive_int, - vol.Optional(CONF_ADDRESS): cv.byte + vol.Optional(CONF_ADDRESS): cv.byte, } ]) }) diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py index 69b03a36769..622b98223aa 100644 --- a/homeassistant/components/raspihats/__init__.py +++ b/homeassistant/components/raspihats/__init__.py @@ -14,7 +14,6 @@ DOMAIN = 'raspihats' CONF_I2C_HATS = 'i2c_hats' CONF_BOARD = 'board' -CONF_ADDRESS = 'address' CONF_CHANNELS = 'channels' CONF_INDEX = 'index' CONF_INVERT_LOGIC = 'invert_logic' diff --git a/homeassistant/components/raspihats/binary_sensor.py b/homeassistant/components/raspihats/binary_sensor.py index 04885402e72..b0ebc2e3579 100644 --- a/homeassistant/components/raspihats/binary_sensor.py +++ b/homeassistant/components/raspihats/binary_sensor.py @@ -6,10 +6,10 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorDevice) from homeassistant.components.raspihats import ( - CONF_ADDRESS, CONF_BOARD, CONF_CHANNELS, CONF_I2C_HATS, CONF_INDEX, - CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException) + CONF_BOARD, CONF_CHANNELS, CONF_I2C_HATS, CONF_INDEX, CONF_INVERT_LOGIC, + I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException) from homeassistant.const import ( - CONF_DEVICE_CLASS, CONF_NAME, DEVICE_DEFAULT_NAME) + CONF_ADDRESS, CONF_DEVICE_CLASS, CONF_NAME, DEVICE_DEFAULT_NAME) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/raspihats/switch.py b/homeassistant/components/raspihats/switch.py index 10bb2f748c4..26fcda3c8d7 100644 --- a/homeassistant/components/raspihats/switch.py +++ b/homeassistant/components/raspihats/switch.py @@ -4,11 +4,10 @@ import logging import voluptuous as vol from homeassistant.components.raspihats import ( - CONF_ADDRESS, CONF_BOARD, CONF_CHANNELS, CONF_I2C_HATS, CONF_INDEX, - CONF_INITIAL_STATE, CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, - I2CHatsException) + CONF_BOARD, CONF_CHANNELS, CONF_I2C_HATS, CONF_INDEX, CONF_INITIAL_STATE, + CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException) from homeassistant.components.switch import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME +from homeassistant.const import CONF_ADDRESS, CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity diff --git a/homeassistant/components/sensor/etherscan.py b/homeassistant/components/sensor/etherscan.py index 3b76d888e26..082295bfea5 100644 --- a/homeassistant/components/sensor/etherscan.py +++ b/homeassistant/components/sensor/etherscan.py @@ -1,15 +1,11 @@ -""" -Support for Etherscan sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.etherscan/ -""" +"""Support for Etherscan sensors.""" from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME, CONF_TOKEN) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -17,8 +13,6 @@ REQUIREMENTS = ['python-etherscan-api==0.0.3'] ATTRIBUTION = "Data provided by etherscan.io" -CONF_ADDRESS = 'address' -CONF_TOKEN = 'token' CONF_TOKEN_ADDRESS = 'token_address' SCAN_INTERVAL = timedelta(minutes=5) diff --git a/homeassistant/components/sensor/ripple.py b/homeassistant/components/sensor/ripple.py index 4abbc4efe06..54530571c3e 100644 --- a/homeassistant/components/sensor/ripple.py +++ b/homeassistant/components/sensor/ripple.py @@ -1,21 +1,15 @@ -""" -Support for Ripple sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.ripple/ -""" +"""Support for Ripple sensors.""" from datetime import timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity REQUIREMENTS = ['python-ripple-api==0.0.3'] -CONF_ADDRESS = 'address' ATTRIBUTION = "Data provided by ripple.com" DEFAULT_NAME = 'Ripple Balance' diff --git a/homeassistant/components/sensor/sochain.py b/homeassistant/components/sensor/sochain.py index d5bd4e5da82..ef6a53b7091 100644 --- a/homeassistant/components/sensor/sochain.py +++ b/homeassistant/components/sensor/sochain.py @@ -1,19 +1,14 @@ -""" -Support for watching multiple cryptocurrencies. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.sochain/ -""" -import logging +"""Support for watching multiple cryptocurrencies.""" from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION) -from homeassistant.helpers.entity import Entity +from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity REQUIREMENTS = ['python-sochain-api==0.0.2'] @@ -21,7 +16,6 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Data provided by chain.so" -CONF_ADDRESS = 'address' CONF_NETWORK = 'network' DEFAULT_NAME = 'Crypto Balance' From 98c3c02daa11e80290f96043aae9670621f51a05 Mon Sep 17 00:00:00 2001 From: Julien Brochet Date: Tue, 19 Feb 2019 16:01:47 +0100 Subject: [PATCH 073/253] Bump Synology SRM dependency to version 0.0.6 (#21212) * Bump Synology SRM dependency to version 0.0.6 * Add @aerialls to the Synology SRM code owners --- CODEOWNERS | 1 + homeassistant/components/device_tracker/synology_srm.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index fc3ba96097c..64c651752e7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -68,6 +68,7 @@ homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/device_tracker/traccar.py @ludeeus homeassistant/components/device_tracker/bt_smarthub.py @jxwolstenholme +homeassistant/components/device_tracker/synology_srm.py @aerialls homeassistant/components/history_graph/* @andrey-git homeassistant/components/influx/* @fabaff homeassistant/components/light/lifx_legacy.py @amelchio diff --git a/homeassistant/components/device_tracker/synology_srm.py b/homeassistant/components/device_tracker/synology_srm.py index 5c7ac9a5d00..bf5653d681b 100644 --- a/homeassistant/components/device_tracker/synology_srm.py +++ b/homeassistant/components/device_tracker/synology_srm.py @@ -13,7 +13,7 @@ from homeassistant.const import ( CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL) -REQUIREMENTS = ['synology-srm==0.0.4'] +REQUIREMENTS = ['synology-srm==0.0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 901a1b2bbeb..92db76c2c5e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1635,7 +1635,7 @@ suds-py3==1.3.3.0 swisshydrodata==0.0.3 # homeassistant.components.device_tracker.synology_srm -synology-srm==0.0.4 +synology-srm==0.0.6 # homeassistant.components.tahoma tahoma-api==0.0.14 From e3cfcbad69b28b86f8d4d934a9d0aec0a08183ea Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 19 Feb 2019 16:04:56 +0100 Subject: [PATCH 074/253] Upgrade numpy to 1.16.1 (#21190) --- homeassistant/components/binary_sensor/trend.py | 9 ++------- homeassistant/components/image_processing/opencv.py | 2 +- .../components/image_processing/tensorflow.py | 12 ++---------- homeassistant/components/sensor/pollen.py | 9 ++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 9 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 494c3154b84..0d4e9631650 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -1,9 +1,4 @@ -""" -A sensor that monitors trends in other components. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.trend/ -""" +"""A sensor that monitors trends in other components.""" from collections import deque import logging import math @@ -22,7 +17,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.16.0'] +REQUIREMENTS = ['numpy==1.16.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index 8ca6e4d8a53..7cb5184b116 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.16.0'] +REQUIREMENTS = ['numpy==1.16.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/tensorflow.py b/homeassistant/components/image_processing/tensorflow.py index cc25756f2d0..f0e8f5182fc 100644 --- a/homeassistant/components/image_processing/tensorflow.py +++ b/homeassistant/components/image_processing/tensorflow.py @@ -1,12 +1,4 @@ -""" -Support for performing TensorFlow classification on images. - -For a quick start, pick a pre-trained COCO model from: -https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/image_processing.tensorflow/ -""" +"""Support for performing TensorFlow classification on images.""" import logging import os import sys @@ -20,7 +12,7 @@ from homeassistant.core import split_entity_id from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.16.0', 'pillow==5.4.1', 'protobuf==3.6.1'] +REQUIREMENTS = ['numpy==1.16.1', 'pillow==5.4.1', 'protobuf==3.6.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index eab5a14b8ca..d553dd8730f 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -1,9 +1,4 @@ -""" -Support for Pollen.com allergen and cold/flu sensors. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.pollen/ -""" +"""Support for Pollen.com allergen and cold/flu sensors.""" from datetime import timedelta import logging from statistics import mean @@ -18,7 +13,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['numpy==1.16.0', 'pypollencom==2.2.2'] +REQUIREMENTS = ['numpy==1.16.1', 'pypollencom==2.2.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 92db76c2c5e..b24b99d7e84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -752,7 +752,7 @@ nuheat==0.3.0 # homeassistant.components.image_processing.opencv # homeassistant.components.image_processing.tensorflow # homeassistant.components.sensor.pollen -numpy==1.16.0 +numpy==1.16.1 # homeassistant.components.google oauth2client==4.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82a5e4ba8b6..67eda186468 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -151,7 +151,7 @@ mficlient==0.3.0 # homeassistant.components.image_processing.opencv # homeassistant.components.image_processing.tensorflow # homeassistant.components.sensor.pollen -numpy==1.16.0 +numpy==1.16.1 # homeassistant.components.mqtt # homeassistant.components.shiftr From 99eda385d103cfac649196a6bb4d91312bf5d2d5 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Tue, 19 Feb 2019 09:44:42 -0700 Subject: [PATCH 075/253] Set aioharmony version to 0.1.8 (#21213) Update aioharmony version to support latest HUB firmware (4.15.250). --- homeassistant/components/harmony/remote.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 612ea735aaa..4ea199bdcd1 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady from homeassistant.util import slugify -REQUIREMENTS = ['aioharmony==0.1.5'] +REQUIREMENTS = ['aioharmony==0.1.8'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index b24b99d7e84..f91e2727c54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -111,7 +111,7 @@ aiofreepybox==0.0.6 aioftp==0.12.0 # homeassistant.components.harmony.remote -aioharmony==0.1.5 +aioharmony==0.1.8 # homeassistant.components.emulated_hue # homeassistant.components.http From b0f317743b9ceaee0e98e10247205dcb6c0ea12b Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 19 Feb 2019 16:45:21 +0000 Subject: [PATCH 076/253] ordered by last occurence (#21200) --- homeassistant/components/system_log/__init__.py | 10 +++++++--- tests/components/system_log/test_init.py | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 9e968111c9c..16786bdeba4 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -88,7 +88,7 @@ class LogEntry: def __init__(self, record, stack, source): """Initialize a log entry.""" - self.timestamp = record.created + self.first_occured = self.timestamp = record.created self.level = record.levelname self.message = record.getMessage() if record.exc_info: @@ -125,9 +125,13 @@ class DedupStore(OrderedDict): key = str(entry.hash()) if key in self: - entry.count = self[key].count + 1 + # Update stored entry + self[key].count += 1 + self[key].timestamp = entry.timestamp - self[key] = entry + self.move_to_end(key) + else: + self[key] = entry if len(self) > self.maxlen: # Removes the first record which should also be the oldest diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index c1d79c9f33f..14047399aff 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -152,6 +152,11 @@ async def test_dedup_logs(hass, hass_client): assert log[1]["count"] == 2 assert_log(log[1], '', 'error message 2', 'ERROR') + _LOGGER.error('error message 2') + log = await get_error_log(hass, hass_client, 2) + assert_log(log[0], '', 'error message 2', 'ERROR') + assert log[0]["timestamp"] > log[0]["first_occured"] + async def test_clear_logs(hass, hass_client): """Test that the log can be cleared via a service call.""" From df8589c36af0941d0f55b38b104ec7d35798a248 Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Tue, 19 Feb 2019 18:42:00 +0100 Subject: [PATCH 077/253] Push pyads to 3.0.7 (#21216) * Push to pyads 3.0.7 * Correct too long line --- homeassistant/components/ads/__init__.py | 9 +++++---- requirements_all.txt | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 48b5ea21cbc..cfd0f37caa0 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -9,7 +9,7 @@ from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \ EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyads==2.2.6'] +REQUIREMENTS = ['pyads==3.0.7'] _LOGGER = logging.getLogger(__name__) @@ -73,9 +73,10 @@ def setup(hass, config): try: ads = AdsHub(client) - except pyads.pyads.ADSError: + except pyads.ADSError: _LOGGER.error( - "Could not connect to ADS host (netid=%s, port=%s)", net_id, port) + "Could not connect to ADS host (netid=%s, ip=%s, port=%s)", + net_id, ip_address, port) return False hass.data[DATA_ADS] = ads @@ -168,7 +169,7 @@ class AdsHub: self._notification_items[hnotify] = NotificationItem( hnotify, huser, name, plc_datatype, callback) - def _device_notification_callback(self, addr, notification, huser): + def _device_notification_callback(self, notification, name): """Handle device notifications.""" contents = notification.contents diff --git a/requirements_all.txt b/requirements_all.txt index f91e2727c54..a380688fa86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -916,7 +916,7 @@ pyW800rf32==0.1 # py_noaa==0.3.0 # homeassistant.components.ads -pyads==2.2.6 +pyads==3.0.7 # homeassistant.components.sensor.aftership pyaftership==0.1.2 From fe4a2b5b31c6943dc6561d80c1f94a50a9b61372 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Tue, 19 Feb 2019 18:53:20 +0100 Subject: [PATCH 078/253] Fix Homematic IP Cloud configuration (#21202) `homematicip.aio.auth.isRequestAcknowledged` returns false if the request failed in stead of raising an error. See coreGreenberet/homematicip-rest-api@0b61954f6accf773a32cf91772b0a5b1bb7ef4ac Closes: #20428 --- homeassistant/components/homematicip_cloud/hap.py | 3 +-- tests/components/homematicip_cloud/test_hap.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 4acace4a8b1..64721c0a96c 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -39,8 +39,7 @@ class HomematicipAuth: from homematicip.base.base_connection import HmipConnectionError try: - await self.auth.isRequestAcknowledged() - return True + return await self.auth.isRequestAcknowledged() except HmipConnectionError: return False diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index c39e7d4e26b..61ca3300d60 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -44,7 +44,7 @@ async def test_auth_auth_check_and_register(hass): hap = hmipc.HomematicipAuth(hass, config) hap.auth = Mock() with patch.object(hap.auth, 'isRequestAcknowledged', - return_value=mock_coro()), \ + return_value=mock_coro(True)), \ patch.object(hap.auth, 'requestAuthToken', return_value=mock_coro('ABC')), \ patch.object(hap.auth, 'confirmAuthToken', From 3be81780356d2514a8aa3651abd0a78d1cefb2d2 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 19 Feb 2019 12:58:22 -0500 Subject: [PATCH 079/253] Refactor ZHA listeners into channels (#21196) * refactor listeners to channels * update coveragerc --- .coveragerc | 2 +- homeassistant/components/zha/__init__.py | 4 +- homeassistant/components/zha/binary_sensor.py | 46 +- homeassistant/components/zha/core/__init__.py | 3 - .../components/zha/core/channels/__init__.py | 308 ++++++++ .../components/zha/core/channels/closures.py | 9 + .../components/zha/core/channels/general.py | 202 +++++ .../zha/core/channels/homeautomation.py | 40 + .../components/zha/core/channels/hvac.py | 62 ++ .../components/zha/core/channels/lighting.py | 48 ++ .../components/zha/core/channels/lightlink.py | 9 + .../zha/core/channels/manufacturerspecific.py | 9 + .../zha/core/channels/measurement.py | 9 + .../components/zha/core/channels/protocol.py | 9 + .../components/zha/core/channels/registry.py | 46 ++ .../components/zha/core/channels/security.py | 82 ++ .../zha/core/channels/smartenergy.py | 9 + homeassistant/components/zha/core/const.py | 20 +- homeassistant/components/zha/core/device.py | 77 +- homeassistant/components/zha/core/gateway.py | 95 +-- .../components/zha/core/listeners.py | 706 ------------------ homeassistant/components/zha/device_entity.py | 25 +- homeassistant/components/zha/entity.py | 26 +- homeassistant/components/zha/fan.py | 14 +- homeassistant/components/zha/light.py | 38 +- homeassistant/components/zha/sensor.py | 20 +- homeassistant/components/zha/switch.py | 12 +- tests/components/zha/conftest.py | 6 +- 28 files changed, 1037 insertions(+), 899 deletions(-) create mode 100644 homeassistant/components/zha/core/channels/__init__.py create mode 100644 homeassistant/components/zha/core/channels/closures.py create mode 100644 homeassistant/components/zha/core/channels/general.py create mode 100644 homeassistant/components/zha/core/channels/homeautomation.py create mode 100644 homeassistant/components/zha/core/channels/hvac.py create mode 100644 homeassistant/components/zha/core/channels/lighting.py create mode 100644 homeassistant/components/zha/core/channels/lightlink.py create mode 100644 homeassistant/components/zha/core/channels/manufacturerspecific.py create mode 100644 homeassistant/components/zha/core/channels/measurement.py create mode 100644 homeassistant/components/zha/core/channels/protocol.py create mode 100644 homeassistant/components/zha/core/channels/registry.py create mode 100644 homeassistant/components/zha/core/channels/security.py create mode 100644 homeassistant/components/zha/core/channels/smartenergy.py delete mode 100644 homeassistant/components/zha/core/listeners.py diff --git a/.coveragerc b/.coveragerc index 281fcc3ec19..df2e0df2aed 100644 --- a/.coveragerc +++ b/.coveragerc @@ -669,11 +669,11 @@ omit = homeassistant/components/zha/__init__.py homeassistant/components/zha/api.py homeassistant/components/zha/const.py + homeassistant/components/zha/core/channels/* homeassistant/components/zha/core/const.py homeassistant/components/zha/core/device.py homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/helpers.py - homeassistant/components/zha/core/listeners.py homeassistant/components/zha/device_entity.py homeassistant/components/zha/entity.py homeassistant/components/zha/light.py diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index b8ef5c40838..6c7e83689ad 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -26,7 +26,7 @@ from .core.const import ( DATA_ZHA_RADIO, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DEFAULT_RADIO_TYPE, DOMAIN, RadioType, DATA_ZHA_CORE_EVENTS, ENABLE_QUIRKS) from .core.gateway import establish_device_mappings -from .core.listeners import populate_listener_registry +from .core.channels.registry import populate_channel_registry REQUIREMENTS = [ 'bellows==0.7.0', @@ -90,7 +90,7 @@ async def async_setup_entry(hass, config_entry): Will automatically load components to support devices found on the network. """ establish_device_mappings() - populate_listener_registry() + populate_channel_registry() for component in COMPONENTS: hass.data[DATA_ZHA][component] = ( diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 1f85373eecc..a46ffdd305d 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -9,9 +9,9 @@ import logging from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_ON_OFF, - LISTENER_LEVEL, LISTENER_ZONE, SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, - SIGNAL_SET_LEVEL, LISTENER_ATTRIBUTE, UNKNOWN, OPENING, ZONE, OCCUPANCY, + DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, ON_OFF_CHANNEL, + LEVEL_CHANNEL, ZONE_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, + SIGNAL_SET_LEVEL, ATTRIBUTE_CHANNEL, UNKNOWN, OPENING, ZONE, OCCUPANCY, ATTR_LEVEL, SENSOR_TYPE) from .entity import ZhaEntity @@ -30,9 +30,9 @@ CLASS_MAPPING = { } -async def get_ias_device_class(listener): - """Get the HA device class from the listener.""" - zone_type = await listener.get_attribute_value('zone_type') +async def get_ias_device_class(channel): + """Get the HA device class from the channel.""" + zone_type = await channel.get_attribute_value('zone_type') return CLASS_MAPPING.get(zone_type) @@ -87,10 +87,10 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): """Initialize the ZHA binary sensor.""" super().__init__(**kwargs) self._device_state_attributes = {} - self._zone_listener = self.cluster_listeners.get(LISTENER_ZONE) - self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF) - self._level_listener = self.cluster_listeners.get(LISTENER_LEVEL) - self._attr_listener = self.cluster_listeners.get(LISTENER_ATTRIBUTE) + self._zone_channel = self.cluster_channels.get(ZONE_CHANNEL) + self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) + self._level_channel = self.cluster_channels.get(LEVEL_CHANNEL) + self._attr_channel = self.cluster_channels.get(ATTRIBUTE_CHANNEL) self._zha_sensor_type = kwargs[SENSOR_TYPE] self._level = None @@ -99,31 +99,31 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): device_class_supplier = DEVICE_CLASS_REGISTRY.get( self._zha_sensor_type) if callable(device_class_supplier): - listener = self.cluster_listeners.get(self._zha_sensor_type) - if listener is None: + channel = self.cluster_channels.get(self._zha_sensor_type) + if channel is None: return None - return await device_class_supplier(listener) + return await device_class_supplier(channel) return device_class_supplier async def async_added_to_hass(self): """Run when about to be added to hass.""" self._device_class = await self._determine_device_class() await super().async_added_to_hass() - if self._level_listener: + if self._level_channel: await self.async_accept_signal( - self._level_listener, SIGNAL_SET_LEVEL, self.set_level) + self._level_channel, SIGNAL_SET_LEVEL, self.set_level) await self.async_accept_signal( - self._level_listener, SIGNAL_MOVE_LEVEL, self.move_level) - if self._on_off_listener: + self._level_channel, SIGNAL_MOVE_LEVEL, self.move_level) + if self._on_off_channel: await self.async_accept_signal( - self._on_off_listener, SIGNAL_ATTR_UPDATED, + self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) - if self._zone_listener: + if self._zone_channel: await self.async_accept_signal( - self._zone_listener, SIGNAL_ATTR_UPDATED, self.async_set_state) - if self._attr_listener: + self._zone_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) + if self._attr_channel: await self.async_accept_signal( - self._attr_listener, SIGNAL_ATTR_UPDATED, self.async_set_state) + self._attr_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) @property def is_on(self) -> bool: @@ -160,7 +160,7 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): @property def device_state_attributes(self): """Return the device state attributes.""" - if self._level_listener is not None: + if self._level_channel is not None: self._device_state_attributes.update({ ATTR_LEVEL: self._state and self._level or 0 }) diff --git a/homeassistant/components/zha/core/__init__.py b/homeassistant/components/zha/core/__init__.py index e7443e7e0b7..145b725fc79 100644 --- a/homeassistant/components/zha/core/__init__.py +++ b/homeassistant/components/zha/core/__init__.py @@ -8,6 +8,3 @@ https://home-assistant.io/components/zha/ # flake8: noqa from .device import ZHADevice from .gateway import ZHAGateway -from .listeners import ( - ClusterListener, AttributeListener, OnOffListener, LevelListener, - IASZoneListener, ActivePowerListener, BatteryListener, EventRelayListener) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py new file mode 100644 index 00000000000..0c0e1ed2173 --- /dev/null +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -0,0 +1,308 @@ +""" +Channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import asyncio +from enum import Enum +from functools import wraps +import logging +from random import uniform + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from ..helpers import ( + bind_configure_reporting, construct_unique_id, + safe_read, get_attr_id_by_name) +from ..const import ( + CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_DEFAULT, SIGNAL_ATTR_UPDATED, + ATTRIBUTE_CHANNEL, EVENT_RELAY_CHANNEL +) + +ZIGBEE_CHANNEL_REGISTRY = {} +_LOGGER = logging.getLogger(__name__) + + +def parse_and_log_command(unique_id, cluster, tsn, command_id, args): + """Parse and log a zigbee cluster command.""" + cmd = cluster.server_commands.get(command_id, [command_id])[0] + _LOGGER.debug( + "%s: received '%s' command with %s args on cluster_id '%s' tsn '%s'", + unique_id, + cmd, + args, + cluster.cluster_id, + tsn + ) + return cmd + + +def decorate_command(channel, command): + """Wrap a cluster command to make it safe.""" + @wraps(command) + async def wrapper(*args, **kwds): + from zigpy.zcl.foundation import Status + from zigpy.exceptions import DeliveryError + try: + result = await command(*args, **kwds) + _LOGGER.debug("%s: executed command: %s %s %s %s", + channel.unique_id, + command.__name__, + "{}: {}".format("with args", args), + "{}: {}".format("with kwargs", kwds), + "{}: {}".format("and result", result)) + if isinstance(result, bool): + return result + return result[1] is Status.SUCCESS + except DeliveryError: + _LOGGER.debug("%s: command failed: %s", channel.unique_id, + command.__name__) + return False + return wrapper + + +class ChannelStatus(Enum): + """Status of a channel.""" + + CREATED = 1 + CONFIGURED = 2 + INITIALIZED = 3 + + +class ZigbeeChannel: + """Base channel for a Zigbee cluster.""" + + def __init__(self, cluster, device): + """Initialize ZigbeeChannel.""" + self.name = 'channel_{}'.format(cluster.cluster_id) + self._cluster = cluster + self._zha_device = device + self._unique_id = construct_unique_id(cluster) + self._report_config = CLUSTER_REPORT_CONFIGS.get( + self._cluster.cluster_id, + [{'attr': 0, 'config': REPORT_CONFIG_DEFAULT}] + ) + self._status = ChannelStatus.CREATED + self._cluster.add_listener(self) + + @property + def unique_id(self): + """Return the unique id for this channel.""" + return self._unique_id + + @property + def cluster(self): + """Return the zigpy cluster for this channel.""" + return self._cluster + + @property + def device(self): + """Return the device this channel is linked to.""" + return self._zha_device + + @property + def status(self): + """Return the status of the channel.""" + return self._status + + def set_report_config(self, report_config): + """Set the reporting configuration.""" + self._report_config = report_config + + async def async_configure(self): + """Set cluster binding and attribute reporting.""" + manufacturer = None + manufacturer_code = self._zha_device.manufacturer_code + if self.cluster.cluster_id >= 0xfc00 and manufacturer_code: + manufacturer = manufacturer_code + + skip_bind = False # bind cluster only for the 1st configured attr + for report_config in self._report_config: + attr = report_config.get('attr') + min_report_interval, max_report_interval, change = \ + report_config.get('config') + await bind_configure_reporting( + self._unique_id, self.cluster, attr, + min_report=min_report_interval, + max_report=max_report_interval, + reportable_change=change, + skip_bind=skip_bind, + manufacturer=manufacturer + ) + skip_bind = True + await asyncio.sleep(uniform(0.1, 0.5)) + _LOGGER.debug( + "%s: finished channel configuration", + self._unique_id + ) + self._status = ChannelStatus.CONFIGURED + + async def async_initialize(self, from_cache): + """Initialize channel.""" + self._status = ChannelStatus.INITIALIZED + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + pass + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + pass + + @callback + def zdo_command(self, *args, **kwargs): + """Handle ZDO commands on this cluster.""" + pass + + @callback + def zha_send_event(self, cluster, command, args): + """Relay events to hass.""" + self._zha_device.hass.bus.async_fire( + 'zha_event', + { + 'unique_id': self._unique_id, + 'device_ieee': str(self._zha_device.ieee), + 'command': command, + 'args': args + } + ) + + async def async_update(self): + """Retrieve latest state from cluster.""" + pass + + async def get_attribute_value(self, attribute, from_cache=True): + """Get the value for an attribute.""" + result = await safe_read( + self._cluster, + [attribute], + allow_cache=from_cache, + only_cache=from_cache + ) + return result.get(attribute) + + def __getattr__(self, name): + """Get attribute or a decorated cluster command.""" + if hasattr(self._cluster, name) and callable( + getattr(self._cluster, name)): + command = getattr(self._cluster, name) + command.__name__ = name + return decorate_command( + self, + command + ) + return self.__getattribute__(name) + + +class AttributeListeningChannel(ZigbeeChannel): + """Channel for attribute reports from the cluster.""" + + def __init__(self, cluster, device): + """Initialize AttributeListeningChannel.""" + super().__init__(cluster, device) + self.name = ATTRIBUTE_CHANNEL + attr = self._report_config[0].get('attr') + if isinstance(attr, str): + self._value_attribute = get_attr_id_by_name(self.cluster, attr) + else: + self._value_attribute = attr + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == self._value_attribute: + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + value + ) + + async def async_initialize(self, from_cache): + """Initialize listener.""" + await self.get_attribute_value( + self._report_config[0].get('attr'), from_cache=from_cache) + await super().async_initialize(from_cache) + + +class ZDOChannel: + """Channel for ZDO events.""" + + def __init__(self, cluster, device): + """Initialize ZDOChannel.""" + self.name = 'zdo' + self._cluster = cluster + self._zha_device = device + self._status = ChannelStatus.CREATED + self._unique_id = "{}_ZDO".format(device.name) + self._cluster.add_listener(self) + + @property + def unique_id(self): + """Return the unique id for this channel.""" + return self._unique_id + + @property + def cluster(self): + """Return the aigpy cluster for this channel.""" + return self._cluster + + @property + def status(self): + """Return the status of the channel.""" + return self._status + + @callback + def device_announce(self, zigpy_device): + """Device announce handler.""" + pass + + @callback + def permit_duration(self, duration): + """Permit handler.""" + pass + + async def async_initialize(self, from_cache): + """Initialize channel.""" + self._status = ChannelStatus.INITIALIZED + + async def async_configure(self): + """Configure channel.""" + self._status = ChannelStatus.CONFIGURED + + +class EventRelayChannel(ZigbeeChannel): + """Event relay that can be attached to zigbee clusters.""" + + def __init__(self, cluster, device): + """Initialize EventRelayChannel.""" + super().__init__(cluster, device) + self.name = EVENT_RELAY_CHANNEL + + @callback + def attribute_updated(self, attrid, value): + """Handle an attribute updated on this cluster.""" + self.zha_send_event( + self._cluster, + SIGNAL_ATTR_UPDATED, + { + 'attribute_id': attrid, + 'attribute_name': self._cluster.attributes.get( + attrid, + ['Unknown'])[0], + 'value': value + } + ) + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle a cluster command received on this cluster.""" + if self._cluster.server_commands is not None and \ + self._cluster.server_commands.get(command_id) is not None: + self.zha_send_event( + self._cluster, + self._cluster.server_commands.get(command_id)[0], + args + ) diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py new file mode 100644 index 00000000000..ba3b6b2e716 --- /dev/null +++ b/homeassistant/components/zha/core/channels/closures.py @@ -0,0 +1,9 @@ +""" +Closures channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py new file mode 100644 index 00000000000..bc015ae47f0 --- /dev/null +++ b/homeassistant/components/zha/core/channels/general.py @@ -0,0 +1,202 @@ +""" +General channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from . import ZigbeeChannel, parse_and_log_command +from ..helpers import get_attr_id_by_name +from ..const import ( + SIGNAL_ATTR_UPDATED, + SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, SIGNAL_STATE_ATTR, BASIC_CHANNEL, + ON_OFF_CHANNEL, LEVEL_CHANNEL, POWER_CONFIGURATION_CHANNEL +) + +_LOGGER = logging.getLogger(__name__) + + +class OnOffChannel(ZigbeeChannel): + """Channel for the OnOff Zigbee cluster.""" + + ON_OFF = 0 + + def __init__(self, cluster, device): + """Initialize OnOffChannel.""" + super().__init__(cluster, device) + self.name = ON_OFF_CHANNEL + self._state = None + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + cmd = parse_and_log_command( + self.unique_id, + self._cluster, + tsn, + command_id, + args + ) + + if cmd in ('off', 'off_with_effect'): + self.attribute_updated(self.ON_OFF, False) + elif cmd in ('on', 'on_with_recall_global_scene', 'on_with_timed_off'): + self.attribute_updated(self.ON_OFF, True) + elif cmd == 'toggle': + self.attribute_updated(self.ON_OFF, not bool(self._state)) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == self.ON_OFF: + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + value + ) + self._state = bool(value) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + self._state = bool( + await self.get_attribute_value(self.ON_OFF, from_cache=from_cache)) + await super().async_initialize(from_cache) + + +class LevelControlChannel(ZigbeeChannel): + """Channel for the LevelControl Zigbee cluster.""" + + CURRENT_LEVEL = 0 + + def __init__(self, cluster, device): + """Initialize LevelControlChannel.""" + super().__init__(cluster, device) + self.name = LEVEL_CHANNEL + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + cmd = parse_and_log_command( + self.unique_id, + self._cluster, + tsn, + command_id, + args + ) + + if cmd in ('move_to_level', 'move_to_level_with_on_off'): + self.dispatch_level_change(SIGNAL_SET_LEVEL, args[0]) + elif cmd in ('move', 'move_with_on_off'): + # We should dim slowly -- for now, just step once + rate = args[1] + if args[0] == 0xff: + rate = 10 # Should read default move rate + self.dispatch_level_change( + SIGNAL_MOVE_LEVEL, -rate if args[0] else rate) + elif cmd in ('step', 'step_with_on_off'): + # Step (technically may change on/off) + self.dispatch_level_change( + SIGNAL_MOVE_LEVEL, -args[1] if args[0] else args[1]) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + _LOGGER.debug("%s: received attribute: %s update with value: %i", + self.unique_id, attrid, value) + if attrid == self.CURRENT_LEVEL: + self.dispatch_level_change(SIGNAL_SET_LEVEL, value) + + def dispatch_level_change(self, command, level): + """Dispatch level change.""" + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, command), + level + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.get_attribute_value( + self.CURRENT_LEVEL, from_cache=from_cache) + await super().async_initialize(from_cache) + + +class BasicChannel(ZigbeeChannel): + """Channel to interact with the basic cluster.""" + + BATTERY = 3 + POWER_SOURCES = { + 0: 'Unknown', + 1: 'Mains (single phase)', + 2: 'Mains (3 phase)', + BATTERY: 'Battery', + 4: 'DC source', + 5: 'Emergency mains constantly powered', + 6: 'Emergency mains and transfer switch' + } + + def __init__(self, cluster, device): + """Initialize BasicChannel.""" + super().__init__(cluster, device) + self.name = BASIC_CHANNEL + self._power_source = None + + async def async_configure(self): + """Configure this channel.""" + await super().async_configure() + await self.async_initialize(False) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + self._power_source = await self.get_attribute_value( + 'power_source', from_cache=from_cache) + await super().async_initialize(from_cache) + + def get_power_source(self): + """Get the power source.""" + return self._power_source + + +class PowerConfigurationChannel(ZigbeeChannel): + """Channel for the zigbee power configuration cluster.""" + + def __init__(self, cluster, device): + """Initialize PowerConfigurationChannel.""" + super().__init__(cluster, device) + self.name = POWER_CONFIGURATION_CHANNEL + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + attr = self._report_config[1].get('attr') + if isinstance(attr, str): + attr_id = get_attr_id_by_name(self.cluster, attr) + else: + attr_id = attr + if attrid == attr_id: + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_STATE_ATTR), + 'battery_level', + value + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.async_read_state(from_cache) + await super().async_initialize(from_cache) + + async def async_update(self): + """Retrieve latest state.""" + await self.async_read_state(True) + + async def async_read_state(self, from_cache): + """Read data from the cluster.""" + await self.get_attribute_value( + 'battery_size', from_cache=from_cache) + await self.get_attribute_value( + 'battery_percentage_remaining', from_cache=from_cache) + await self.get_attribute_value( + 'active_power', from_cache=from_cache) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py new file mode 100644 index 00000000000..2518889fcb1 --- /dev/null +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -0,0 +1,40 @@ +""" +Home automation channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging +from homeassistant.helpers.dispatcher import async_dispatcher_send +from . import AttributeListeningChannel +from ..const import SIGNAL_ATTR_UPDATED, ELECTRICAL_MEASUREMENT_CHANNEL + +_LOGGER = logging.getLogger(__name__) + + +class ElectricalMeasurementChannel(AttributeListeningChannel): + """Channel that polls active power level.""" + + def __init__(self, cluster, device): + """Initialize ElectricalMeasurementChannel.""" + super().__init__(cluster, device) + self.name = ELECTRICAL_MEASUREMENT_CHANNEL + + async def async_update(self): + """Retrieve latest state.""" + _LOGGER.debug("%s async_update", self.unique_id) + + # This is a polling channel. Don't allow cache. + result = await self.get_attribute_value( + ELECTRICAL_MEASUREMENT_CHANNEL, from_cache=False) + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + result + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.get_attribute_value( + ELECTRICAL_MEASUREMENT_CHANNEL, from_cache=from_cache) + await super().async_initialize(from_cache) diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py new file mode 100644 index 00000000000..c62ec66588e --- /dev/null +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -0,0 +1,62 @@ +""" +HVAC channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from . import ZigbeeChannel +from ..const import FAN_CHANNEL, SIGNAL_ATTR_UPDATED + +_LOGGER = logging.getLogger(__name__) + + +class FanChannel(ZigbeeChannel): + """Fan channel.""" + + _value_attribute = 0 + + def __init__(self, cluster, device): + """Initialize FanChannel.""" + super().__init__(cluster, device) + self.name = FAN_CHANNEL + + async def async_set_speed(self, value) -> None: + """Set the speed of the fan.""" + from zigpy.exceptions import DeliveryError + try: + await self.cluster.write_attributes({'fan_mode': value}) + except DeliveryError as ex: + _LOGGER.error("%s: Could not set speed: %s", self.unique_id, ex) + return + + async def async_update(self): + """Retrieve latest state.""" + result = await self.get_attribute_value('fan_mode', from_cache=True) + + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + result + ) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute update from fan cluster.""" + attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + _LOGGER.debug("%s: Attribute report '%s'[%s] = %s", + self.unique_id, self.cluster.name, attr_name, value) + if attrid == self._value_attribute: + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + value + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.get_attribute_value( + self._value_attribute, from_cache=from_cache) + await super().async_initialize(from_cache) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py new file mode 100644 index 00000000000..ee88a30e828 --- /dev/null +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -0,0 +1,48 @@ +""" +Lighting channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging +from . import ZigbeeChannel +from ..const import COLOR_CHANNEL + +_LOGGER = logging.getLogger(__name__) + + +class ColorChannel(ZigbeeChannel): + """Color channel.""" + + CAPABILITIES_COLOR_XY = 0x08 + CAPABILITIES_COLOR_TEMP = 0x10 + UNSUPPORTED_ATTRIBUTE = 0x86 + + def __init__(self, cluster, device): + """Initialize ColorChannel.""" + super().__init__(cluster, device) + self.name = COLOR_CHANNEL + self._color_capabilities = None + + def get_color_capabilities(self): + """Return the color capabilities.""" + return self._color_capabilities + + async def async_initialize(self, from_cache): + """Initialize channel.""" + capabilities = await self.get_attribute_value( + 'color_capabilities', from_cache=from_cache) + + if capabilities is None: + # ZCL Version 4 devices don't support the color_capabilities + # attribute. In this version XY support is mandatory, but we + # need to probe to determine if the device supports color + # temperature. + capabilities = self.CAPABILITIES_COLOR_XY + result = await self.get_attribute_value( + 'color_temperature', from_cache=from_cache) + + if result is not self.UNSUPPORTED_ATTRIBUTE: + capabilities |= self.CAPABILITIES_COLOR_TEMP + self._color_capabilities = capabilities + await super().async_initialize(from_cache) diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py new file mode 100644 index 00000000000..83fca6e80c2 --- /dev/null +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -0,0 +1,9 @@ +""" +Lightlink channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py new file mode 100644 index 00000000000..a0eebd78343 --- /dev/null +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -0,0 +1,9 @@ +""" +Manufacturer specific channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py new file mode 100644 index 00000000000..51146289e69 --- /dev/null +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -0,0 +1,9 @@ +""" +Measurement channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/protocol.py b/homeassistant/components/zha/core/channels/protocol.py new file mode 100644 index 00000000000..2cae156aec5 --- /dev/null +++ b/homeassistant/components/zha/core/channels/protocol.py @@ -0,0 +1,9 @@ +""" +Protocol channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/registry.py b/homeassistant/components/zha/core/channels/registry.py new file mode 100644 index 00000000000..f0363ac8330 --- /dev/null +++ b/homeassistant/components/zha/core/channels/registry.py @@ -0,0 +1,46 @@ +""" +Channel registry module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +from . import ZigbeeChannel +from .general import ( + OnOffChannel, LevelControlChannel, PowerConfigurationChannel, BasicChannel +) +from .homeautomation import ElectricalMeasurementChannel +from .hvac import FanChannel +from .lighting import ColorChannel +from .security import IASZoneChannel + + +ZIGBEE_CHANNEL_REGISTRY = {} + + +def populate_channel_registry(): + """Populate the channel registry.""" + from zigpy import zcl + ZIGBEE_CHANNEL_REGISTRY.update({ + zcl.clusters.general.Alarms.cluster_id: ZigbeeChannel, + zcl.clusters.general.Commissioning.cluster_id: ZigbeeChannel, + zcl.clusters.general.Identify.cluster_id: ZigbeeChannel, + zcl.clusters.general.Groups.cluster_id: ZigbeeChannel, + zcl.clusters.general.Scenes.cluster_id: ZigbeeChannel, + zcl.clusters.general.Partition.cluster_id: ZigbeeChannel, + zcl.clusters.general.Ota.cluster_id: ZigbeeChannel, + zcl.clusters.general.PowerProfile.cluster_id: ZigbeeChannel, + zcl.clusters.general.ApplianceControl.cluster_id: ZigbeeChannel, + zcl.clusters.general.PollControl.cluster_id: ZigbeeChannel, + zcl.clusters.general.GreenPowerProxy.cluster_id: ZigbeeChannel, + zcl.clusters.general.OnOffConfiguration.cluster_id: ZigbeeChannel, + zcl.clusters.general.OnOff.cluster_id: OnOffChannel, + zcl.clusters.general.LevelControl.cluster_id: LevelControlChannel, + zcl.clusters.lighting.Color.cluster_id: ColorChannel, + zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: + ElectricalMeasurementChannel, + zcl.clusters.general.PowerConfiguration.cluster_id: + PowerConfigurationChannel, + zcl.clusters.general.Basic.cluster_id: BasicChannel, + zcl.clusters.security.IasZone.cluster_id: IASZoneChannel, + zcl.clusters.hvac.Fan.cluster_id: FanChannel, + }) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py new file mode 100644 index 00000000000..e8c0e71a263 --- /dev/null +++ b/homeassistant/components/zha/core/channels/security.py @@ -0,0 +1,82 @@ +""" +Security channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from . import ZigbeeChannel +from ..helpers import bind_cluster +from ..const import SIGNAL_ATTR_UPDATED, ZONE_CHANNEL + +_LOGGER = logging.getLogger(__name__) + + +class IASZoneChannel(ZigbeeChannel): + """Channel for the IASZone Zigbee cluster.""" + + def __init__(self, cluster, device): + """Initialize IASZoneChannel.""" + super().__init__(cluster, device) + self.name = ZONE_CHANNEL + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + if command_id == 0: + state = args[0] & 3 + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + state + ) + _LOGGER.debug("Updated alarm state: %s", state) + elif command_id == 1: + _LOGGER.debug("Enroll requested") + res = self._cluster.enroll_response(0, 0) + self._zha_device.hass.async_create_task(res) + + async def async_configure(self): + """Configure IAS device.""" + from zigpy.exceptions import DeliveryError + _LOGGER.debug("%s: started IASZoneChannel configuration", + self._unique_id) + + await bind_cluster(self.unique_id, self._cluster) + ieee = self._cluster.endpoint.device.application.ieee + + try: + res = await self._cluster.write_attributes({'cie_addr': ieee}) + _LOGGER.debug( + "%s: wrote cie_addr: %s to '%s' cluster: %s", + self.unique_id, str(ieee), self._cluster.ep_attribute, + res[0] + ) + except DeliveryError as ex: + _LOGGER.debug( + "%s: Failed to write cie_addr: %s to '%s' cluster: %s", + self.unique_id, str(ieee), self._cluster.ep_attribute, str(ex) + ) + _LOGGER.debug("%s: finished IASZoneChannel configuration", + self._unique_id) + + await self.get_attribute_value('zone_type', from_cache=False) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == 2: + value = value & 3 + async_dispatcher_send( + self._zha_device.hass, + "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), + value + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.get_attribute_value('zone_status', from_cache=from_cache) + await self.get_attribute_value('zone_state', from_cache=from_cache) + await super().async_initialize(from_cache) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py new file mode 100644 index 00000000000..d17eae30a96 --- /dev/null +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -0,0 +1,9 @@ +""" +Smart energy channels module for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zha/ +""" +import logging + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index faa423d8ac4..d1001682c7b 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -70,16 +70,16 @@ OCCUPANCY = 'occupancy' ATTR_LEVEL = 'level' -LISTENER_ON_OFF = 'on_off' -LISTENER_ATTRIBUTE = 'attribute' -LISTENER_BASIC = 'basic' -LISTENER_COLOR = 'color' -LISTENER_FAN = 'fan' -LISTENER_LEVEL = ATTR_LEVEL -LISTENER_ZONE = 'zone' -LISTENER_ACTIVE_POWER = 'active_power' -LISTENER_BATTERY = 'battery' -LISTENER_EVENT_RELAY = 'event_relay' +ON_OFF_CHANNEL = 'on_off' +ATTRIBUTE_CHANNEL = 'attribute' +BASIC_CHANNEL = 'basic' +COLOR_CHANNEL = 'color' +FAN_CHANNEL = 'fan' +LEVEL_CHANNEL = ATTR_LEVEL +ZONE_CHANNEL = 'zone' +ELECTRICAL_MEASUREMENT_CHANNEL = 'active_power' +POWER_CONFIGURATION_CHANNEL = 'battery' +EVENT_RELAY_CHANNEL = 'event_relay' SIGNAL_ATTR_UPDATED = 'attribute_updated' SIGNAL_MOVE_LEVEL = "move_level" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 7bb39f943f6..3a012ed7895 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -11,13 +11,14 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send ) from .const import ( - ATTR_MANUFACTURER, LISTENER_BATTERY, SIGNAL_AVAILABLE, IN, OUT, + ATTR_MANUFACTURER, POWER_CONFIGURATION_CHANNEL, SIGNAL_AVAILABLE, IN, OUT, ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER, ATTR_COMMAND_TYPE, ATTR_ARGS, CLIENT_COMMANDS, SERVER_COMMANDS, ATTR_ENDPOINT_ID, IEEE, MODEL, NAME, UNKNOWN, QUIRK_APPLIED, - QUIRK_CLASS, LISTENER_BASIC + QUIRK_CLASS, BASIC_CHANNEL ) -from .listeners import EventRelayListener, BasicListener +from .channels import EventRelayChannel +from .channels.general import BasicChannel _LOGGER = logging.getLogger(__name__) @@ -38,9 +39,9 @@ class ZHADevice: self._manufacturer = zigpy_device.endpoints[ept_id].manufacturer self._model = zigpy_device.endpoints[ept_id].model self._zha_gateway = zha_gateway - self.cluster_listeners = {} - self._relay_listeners = [] - self._all_listeners = [] + self.cluster_channels = {} + self._relay_channels = [] + self._all_channels = [] self._name = "{} {}".format( self.manufacturer, self.model @@ -113,9 +114,9 @@ class ZHADevice: return self._zha_gateway @property - def all_listeners(self): - """Return cluster listeners and relay listeners for device.""" - return self._all_listeners + def all_channels(self): + """Return cluster channels and relay channels for device.""" + return self._all_channels @property def available_signal(self): @@ -156,59 +157,59 @@ class ZHADevice: QUIRK_CLASS: self.quirk_class } - def add_cluster_listener(self, cluster_listener): - """Add cluster listener to device.""" - # only keep 1 power listener - if cluster_listener.name is LISTENER_BATTERY and \ - LISTENER_BATTERY in self.cluster_listeners: + def add_cluster_channel(self, cluster_channel): + """Add cluster channel to device.""" + # only keep 1 power configuration channel + if cluster_channel.name is POWER_CONFIGURATION_CHANNEL and \ + POWER_CONFIGURATION_CHANNEL in self.cluster_channels: return - self._all_listeners.append(cluster_listener) - if isinstance(cluster_listener, EventRelayListener): - self._relay_listeners.append(cluster_listener) + self._all_channels.append(cluster_channel) + if isinstance(cluster_channel, EventRelayChannel): + self._relay_channels.append(cluster_channel) else: - self.cluster_listeners[cluster_listener.name] = cluster_listener + self.cluster_channels[cluster_channel.name] = cluster_channel async def async_configure(self): """Configure the device.""" _LOGGER.debug('%s: started configuration', self.name) - await self._execute_listener_tasks('async_configure') + await self._execute_channel_tasks('async_configure') _LOGGER.debug('%s: completed configuration', self.name) async def async_initialize(self, from_cache=False): - """Initialize listeners.""" + """Initialize channels.""" _LOGGER.debug('%s: started initialization', self.name) - await self._execute_listener_tasks('async_initialize', from_cache) - self.power_source = self.cluster_listeners.get( - LISTENER_BASIC).get_power_source() + await self._execute_channel_tasks('async_initialize', from_cache) + self.power_source = self.cluster_channels.get( + BASIC_CHANNEL).get_power_source() _LOGGER.debug( '%s: power source: %s', self.name, - BasicListener.POWER_SOURCES.get(self.power_source) + BasicChannel.POWER_SOURCES.get(self.power_source) ) _LOGGER.debug('%s: completed initialization', self.name) - async def _execute_listener_tasks(self, task_name, *args): - """Gather and execute a set of listener tasks.""" - listener_tasks = [] - for listener in self.all_listeners: - listener_tasks.append( - self._async_create_task(listener, task_name, *args)) - await asyncio.gather(*listener_tasks) + async def _execute_channel_tasks(self, task_name, *args): + """Gather and execute a set of CHANNEL tasks.""" + channel_tasks = [] + for channel in self.all_channels: + channel_tasks.append( + self._async_create_task(channel, task_name, *args)) + await asyncio.gather(*channel_tasks) - async def _async_create_task(self, listener, func_name, *args): - """Configure a single listener on this device.""" + async def _async_create_task(self, channel, func_name, *args): + """Configure a single channel on this device.""" try: - await getattr(listener, func_name)(*args) - _LOGGER.debug('%s: listener: %s %s stage succeeded', + await getattr(channel, func_name)(*args) + _LOGGER.debug('%s: channel: %s %s stage succeeded', self.name, "{}-{}".format( - listener.name, listener.unique_id), + channel.name, channel.unique_id), func_name) except Exception as ex: # pylint: disable=broad-except _LOGGER.warning( - '%s listener: %s %s stage failed ex: %s', + '%s channel: %s %s stage failed ex: %s', self.name, - "{}-{}".format(listener.name, listener.unique_id), + "{}-{}".format(channel.name, channel.unique_id), func_name, ex ) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 391b12189cf..4fbf96a22b6 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -18,15 +18,18 @@ from .const import ( ZHA_DISCOVERY_NEW, DEVICE_CLASS, SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, COMPONENT_CLUSTERS, HUMIDITY, TEMPERATURE, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT, - GENERIC, SENSOR_TYPE, EVENT_RELAY_CLUSTERS, LISTENER_BATTERY, UNKNOWN, + GENERIC, SENSOR_TYPE, EVENT_RELAY_CLUSTERS, UNKNOWN, OPENING, ZONE, OCCUPANCY, CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_IMMEDIATE, REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT, - REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, SIGNAL_REMOVE, NO_SENSOR_CLUSTERS) + REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, SIGNAL_REMOVE, NO_SENSOR_CLUSTERS, + POWER_CONFIGURATION_CHANNEL) from .device import ZHADevice from ..device_entity import ZhaDeviceEntity -from .listeners import ( - LISTENER_REGISTRY, AttributeListener, EventRelayListener, ZDOListener, - BasicListener) +from .channels import ( + AttributeListeningChannel, EventRelayChannel, ZDOChannel +) +from .channels.general import BasicChannel +from .channels.registry import ZIGBEE_CHANNEL_REGISTRY from .helpers import convert_ieee _LOGGER = logging.getLogger(__name__) @@ -34,7 +37,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = {} BINARY_SENSOR_TYPES = {} EntityReference = collections.namedtuple( - 'EntityReference', 'reference_id zha_device cluster_listeners device_info') + 'EntityReference', 'reference_id zha_device cluster_channels device_info') class ZHAGateway: @@ -106,14 +109,14 @@ class ZHAGateway: return self._device_registry def register_entity_reference( - self, ieee, reference_id, zha_device, cluster_listeners, + self, ieee, reference_id, zha_device, cluster_channels, device_info): """Record the creation of a hass entity associated with ieee.""" self._device_registry[ieee].append( EntityReference( reference_id=reference_id, zha_device=zha_device, - cluster_listeners=cluster_listeners, + cluster_channels=cluster_channels, device_info=device_info ) ) @@ -169,14 +172,14 @@ class ZHAGateway: # available and we already loaded fresh state above zha_device.update_available(True) elif not zha_device.available and zha_device.power_source is not None\ - and zha_device.power_source != BasicListener.BATTERY: + and zha_device.power_source != BasicChannel.BATTERY: # the device is currently marked unavailable and it isn't a battery # powered device so we should be able to update it now _LOGGER.debug( "attempting to request fresh state for %s %s", zha_device.name, "with power source: {}".format( - BasicListener.POWER_SOURCES.get(zha_device.power_source) + BasicChannel.POWER_SOURCES.get(zha_device.power_source) ) ) await zha_device.async_initialize(from_cache=False) @@ -188,11 +191,11 @@ class ZHAGateway: import zigpy.profiles if endpoint_id == 0: # ZDO - await _create_cluster_listener( + await _create_cluster_channel( endpoint, zha_device, is_new_join, - listener_class=ZDOListener + channel_class=ZDOChannel ) return @@ -234,18 +237,18 @@ class ZHAGateway: )) -async def _create_cluster_listener(cluster, zha_device, is_new_join, - listeners=None, listener_class=None): - """Create a cluster listener and attach it to a device.""" - if listener_class is None: - listener_class = LISTENER_REGISTRY.get(cluster.cluster_id, - AttributeListener) - listener = listener_class(cluster, zha_device) +async def _create_cluster_channel(cluster, zha_device, is_new_join, + channels=None, channel_class=None): + """Create a cluster channel and attach it to a device.""" + if channel_class is None: + channel_class = ZIGBEE_CHANNEL_REGISTRY.get(cluster.cluster_id, + AttributeListeningChannel) + channel = channel_class(cluster, zha_device) if is_new_join: - await listener.async_configure() - zha_device.add_cluster_listener(listener) - if listeners is not None: - listeners.append(listener) + await channel.async_configure() + zha_device.add_cluster_channel(channel) + if channels is not None: + channels.append(channel) async def _dispatch_discovery_info(hass, is_new_join, discovery_info): @@ -272,23 +275,23 @@ async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device, for c in profile_clusters[1] if c in endpoint.out_clusters] - listeners = [] + channels = [] cluster_tasks = [] for cluster in in_clusters: - cluster_tasks.append(_create_cluster_listener( - cluster, zha_device, is_new_join, listeners=listeners)) + cluster_tasks.append(_create_cluster_channel( + cluster, zha_device, is_new_join, channels=channels)) for cluster in out_clusters: - cluster_tasks.append(_create_cluster_listener( - cluster, zha_device, is_new_join, listeners=listeners)) + cluster_tasks.append(_create_cluster_channel( + cluster, zha_device, is_new_join, channels=channels)) await asyncio.gather(*cluster_tasks) discovery_info = { 'unique_id': device_key, 'zha_device': zha_device, - 'listeners': listeners, + 'channels': channels, 'component': component } @@ -314,7 +317,7 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, """Dispatch single cluster matches to HA components.""" cluster_matches = [] cluster_match_tasks = [] - event_listener_tasks = [] + event_channel_tasks = [] for cluster in endpoint.in_clusters.values(): if cluster.cluster_id not in profile_clusters[0]: cluster_match_tasks.append(_handle_single_cluster_match( @@ -327,7 +330,7 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, )) if cluster.cluster_id in NO_SENSOR_CLUSTERS: - cluster_match_tasks.append(_handle_listener_only_cluster_match( + cluster_match_tasks.append(_handle_channel_only_cluster_match( zha_device, cluster, is_new_join, @@ -345,13 +348,13 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, )) if cluster.cluster_id in EVENT_RELAY_CLUSTERS: - event_listener_tasks.append(_create_cluster_listener( + event_channel_tasks.append(_create_cluster_channel( cluster, zha_device, is_new_join, - listener_class=EventRelayListener + channel_class=EventRelayChannel )) - await asyncio.gather(*event_listener_tasks) + await asyncio.gather(*event_channel_tasks) cluster_match_results = await asyncio.gather(*cluster_match_tasks) for cluster_match in cluster_match_results: if cluster_match is not None: @@ -359,10 +362,10 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, return cluster_matches -async def _handle_listener_only_cluster_match( +async def _handle_channel_only_cluster_match( zha_device, cluster, is_new_join): - """Handle a listener only cluster match.""" - await _create_cluster_listener(cluster, zha_device, is_new_join) + """Handle a channel only cluster match.""" + await _create_cluster_channel(cluster, zha_device, is_new_join) async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, @@ -376,15 +379,15 @@ async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, if component is None or component not in COMPONENTS: return - listeners = [] - await _create_cluster_listener(cluster, zha_device, is_new_join, - listeners=listeners) + channels = [] + await _create_cluster_channel(cluster, zha_device, is_new_join, + channels=channels) cluster_key = "{}-{}".format(device_key, cluster.cluster_id) discovery_info = { 'unique_id': cluster_key, 'zha_device': zha_device, - 'listeners': listeners, + 'channels': channels, 'entity_suffix': '_{}'.format(cluster.cluster_id), 'component': component } @@ -403,11 +406,11 @@ async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, def _create_device_entity(zha_device): """Create ZHADeviceEntity.""" - device_entity_listeners = [] - if LISTENER_BATTERY in zha_device.cluster_listeners: - listener = zha_device.cluster_listeners.get(LISTENER_BATTERY) - device_entity_listeners.append(listener) - return ZhaDeviceEntity(zha_device, device_entity_listeners) + device_entity_channels = [] + if POWER_CONFIGURATION_CHANNEL in zha_device.cluster_channels: + channel = zha_device.cluster_channels.get(POWER_CONFIGURATION_CHANNEL) + device_entity_channels.append(channel) + return ZhaDeviceEntity(zha_device, device_entity_channels) def establish_device_mappings(): diff --git a/homeassistant/components/zha/core/listeners.py b/homeassistant/components/zha/core/listeners.py deleted file mode 100644 index f8d24ce903c..00000000000 --- a/homeassistant/components/zha/core/listeners.py +++ /dev/null @@ -1,706 +0,0 @@ -""" -Cluster listeners for Zigbee Home Automation. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ -""" - -import asyncio -from enum import Enum -from functools import wraps -import logging -from random import uniform - -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_send -from .helpers import ( - bind_configure_reporting, construct_unique_id, - safe_read, get_attr_id_by_name, bind_cluster) -from .const import ( - CLUSTER_REPORT_CONFIGS, REPORT_CONFIG_DEFAULT, SIGNAL_ATTR_UPDATED, - SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, SIGNAL_STATE_ATTR, LISTENER_BASIC, - LISTENER_ATTRIBUTE, LISTENER_ON_OFF, LISTENER_COLOR, LISTENER_FAN, - LISTENER_LEVEL, LISTENER_ZONE, LISTENER_ACTIVE_POWER, LISTENER_BATTERY, - LISTENER_EVENT_RELAY -) - -LISTENER_REGISTRY = {} - -_LOGGER = logging.getLogger(__name__) - - -def populate_listener_registry(): - """Populate the listener registry.""" - from zigpy import zcl - LISTENER_REGISTRY.update({ - zcl.clusters.general.Alarms.cluster_id: ClusterListener, - zcl.clusters.general.Commissioning.cluster_id: ClusterListener, - zcl.clusters.general.Identify.cluster_id: ClusterListener, - zcl.clusters.general.Groups.cluster_id: ClusterListener, - zcl.clusters.general.Scenes.cluster_id: ClusterListener, - zcl.clusters.general.Partition.cluster_id: ClusterListener, - zcl.clusters.general.Ota.cluster_id: ClusterListener, - zcl.clusters.general.PowerProfile.cluster_id: ClusterListener, - zcl.clusters.general.ApplianceControl.cluster_id: ClusterListener, - zcl.clusters.general.PollControl.cluster_id: ClusterListener, - zcl.clusters.general.GreenPowerProxy.cluster_id: ClusterListener, - zcl.clusters.general.OnOffConfiguration.cluster_id: ClusterListener, - zcl.clusters.general.OnOff.cluster_id: OnOffListener, - zcl.clusters.general.LevelControl.cluster_id: LevelListener, - zcl.clusters.lighting.Color.cluster_id: ColorListener, - zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: - ActivePowerListener, - zcl.clusters.general.PowerConfiguration.cluster_id: BatteryListener, - zcl.clusters.general.Basic.cluster_id: BasicListener, - zcl.clusters.security.IasZone.cluster_id: IASZoneListener, - zcl.clusters.hvac.Fan.cluster_id: FanListener, - }) - - -def parse_and_log_command(unique_id, cluster, tsn, command_id, args): - """Parse and log a zigbee cluster command.""" - cmd = cluster.server_commands.get(command_id, [command_id])[0] - _LOGGER.debug( - "%s: received '%s' command with %s args on cluster_id '%s' tsn '%s'", - unique_id, - cmd, - args, - cluster.cluster_id, - tsn - ) - return cmd - - -def decorate_command(listener, command): - """Wrap a cluster command to make it safe.""" - @wraps(command) - async def wrapper(*args, **kwds): - from zigpy.zcl.foundation import Status - from zigpy.exceptions import DeliveryError - try: - result = await command(*args, **kwds) - _LOGGER.debug("%s: executed command: %s %s %s %s", - listener.unique_id, - command.__name__, - "{}: {}".format("with args", args), - "{}: {}".format("with kwargs", kwds), - "{}: {}".format("and result", result)) - if isinstance(result, bool): - return result - return result[1] is Status.SUCCESS - except DeliveryError: - _LOGGER.debug("%s: command failed: %s", listener.unique_id, - command.__name__) - return False - return wrapper - - -class ListenerStatus(Enum): - """Status of a listener.""" - - CREATED = 1 - CONFIGURED = 2 - INITIALIZED = 3 - - -class ClusterListener: - """Listener for a Zigbee cluster.""" - - def __init__(self, cluster, device): - """Initialize ClusterListener.""" - self.name = 'cluster_{}'.format(cluster.cluster_id) - self._cluster = cluster - self._zha_device = device - self._unique_id = construct_unique_id(cluster) - self._report_config = CLUSTER_REPORT_CONFIGS.get( - self._cluster.cluster_id, - [{'attr': 0, 'config': REPORT_CONFIG_DEFAULT}] - ) - self._status = ListenerStatus.CREATED - self._cluster.add_listener(self) - - @property - def unique_id(self): - """Return the unique id for this listener.""" - return self._unique_id - - @property - def cluster(self): - """Return the zigpy cluster for this listener.""" - return self._cluster - - @property - def device(self): - """Return the device this listener is linked to.""" - return self._zha_device - - @property - def status(self): - """Return the status of the listener.""" - return self._status - - def set_report_config(self, report_config): - """Set the reporting configuration.""" - self._report_config = report_config - - async def async_configure(self): - """Set cluster binding and attribute reporting.""" - manufacturer = None - manufacturer_code = self._zha_device.manufacturer_code - if self.cluster.cluster_id >= 0xfc00 and manufacturer_code: - manufacturer = manufacturer_code - - skip_bind = False # bind cluster only for the 1st configured attr - for report_config in self._report_config: - attr = report_config.get('attr') - min_report_interval, max_report_interval, change = \ - report_config.get('config') - await bind_configure_reporting( - self._unique_id, self.cluster, attr, - min_report=min_report_interval, - max_report=max_report_interval, - reportable_change=change, - skip_bind=skip_bind, - manufacturer=manufacturer - ) - skip_bind = True - await asyncio.sleep(uniform(0.1, 0.5)) - _LOGGER.debug( - "%s: finished listener configuration", - self._unique_id - ) - self._status = ListenerStatus.CONFIGURED - - async def async_initialize(self, from_cache): - """Initialize listener.""" - self._status = ListenerStatus.INITIALIZED - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - pass - - @callback - def attribute_updated(self, attrid, value): - """Handle attribute updates on this cluster.""" - pass - - @callback - def zdo_command(self, *args, **kwargs): - """Handle ZDO commands on this cluster.""" - pass - - @callback - def zha_send_event(self, cluster, command, args): - """Relay events to hass.""" - self._zha_device.hass.bus.async_fire( - 'zha_event', - { - 'unique_id': self._unique_id, - 'device_ieee': str(self._zha_device.ieee), - 'command': command, - 'args': args - } - ) - - async def async_update(self): - """Retrieve latest state from cluster.""" - pass - - async def get_attribute_value(self, attribute, from_cache=True): - """Get the value for an attribute.""" - result = await safe_read( - self._cluster, - [attribute], - allow_cache=from_cache, - only_cache=from_cache - ) - return result.get(attribute) - - def __getattr__(self, name): - """Get attribute or a decorated cluster command.""" - if hasattr(self._cluster, name) and callable( - getattr(self._cluster, name)): - command = getattr(self._cluster, name) - command.__name__ = name - return decorate_command( - self, - command - ) - return self.__getattribute__(name) - - -class AttributeListener(ClusterListener): - """Listener for the attribute reports cluster.""" - - def __init__(self, cluster, device): - """Initialize AttributeListener.""" - super().__init__(cluster, device) - self.name = LISTENER_ATTRIBUTE - attr = self._report_config[0].get('attr') - if isinstance(attr, str): - self._value_attribute = get_attr_id_by_name(self.cluster, attr) - else: - self._value_attribute = attr - - @callback - def attribute_updated(self, attrid, value): - """Handle attribute updates on this cluster.""" - if attrid == self._value_attribute: - async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value - ) - - async def async_initialize(self, from_cache): - """Initialize listener.""" - await self.get_attribute_value( - self._report_config[0].get('attr'), from_cache=from_cache) - await super().async_initialize(from_cache) - - -class OnOffListener(ClusterListener): - """Listener for the OnOff Zigbee cluster.""" - - ON_OFF = 0 - - def __init__(self, cluster, device): - """Initialize OnOffListener.""" - super().__init__(cluster, device) - self.name = LISTENER_ON_OFF - self._state = None - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - cmd = parse_and_log_command( - self.unique_id, - self._cluster, - tsn, - command_id, - args - ) - - if cmd in ('off', 'off_with_effect'): - self.attribute_updated(self.ON_OFF, False) - elif cmd in ('on', 'on_with_recall_global_scene', 'on_with_timed_off'): - self.attribute_updated(self.ON_OFF, True) - elif cmd == 'toggle': - self.attribute_updated(self.ON_OFF, not bool(self._state)) - - @callback - def attribute_updated(self, attrid, value): - """Handle attribute updates on this cluster.""" - if attrid == self.ON_OFF: - async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value - ) - self._state = bool(value) - - async def async_initialize(self, from_cache): - """Initialize listener.""" - self._state = bool( - await self.get_attribute_value(self.ON_OFF, from_cache=from_cache)) - await super().async_initialize(from_cache) - - -class LevelListener(ClusterListener): - """Listener for the LevelControl Zigbee cluster.""" - - CURRENT_LEVEL = 0 - - def __init__(self, cluster, device): - """Initialize LevelListener.""" - super().__init__(cluster, device) - self.name = LISTENER_LEVEL - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - cmd = parse_and_log_command( - self.unique_id, - self._cluster, - tsn, - command_id, - args - ) - - if cmd in ('move_to_level', 'move_to_level_with_on_off'): - self.dispatch_level_change(SIGNAL_SET_LEVEL, args[0]) - elif cmd in ('move', 'move_with_on_off'): - # We should dim slowly -- for now, just step once - rate = args[1] - if args[0] == 0xff: - rate = 10 # Should read default move rate - self.dispatch_level_change( - SIGNAL_MOVE_LEVEL, -rate if args[0] else rate) - elif cmd in ('step', 'step_with_on_off'): - # Step (technically may change on/off) - self.dispatch_level_change( - SIGNAL_MOVE_LEVEL, -args[1] if args[0] else args[1]) - - @callback - def attribute_updated(self, attrid, value): - """Handle attribute updates on this cluster.""" - _LOGGER.debug("%s: received attribute: %s update with value: %i", - self.unique_id, attrid, value) - if attrid == self.CURRENT_LEVEL: - self.dispatch_level_change(SIGNAL_SET_LEVEL, value) - - def dispatch_level_change(self, command, level): - """Dispatch level change.""" - async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, command), - level - ) - - async def async_initialize(self, from_cache): - """Initialize listener.""" - await self.get_attribute_value( - self.CURRENT_LEVEL, from_cache=from_cache) - await super().async_initialize(from_cache) - - -class IASZoneListener(ClusterListener): - """Listener for the IASZone Zigbee cluster.""" - - def __init__(self, cluster, device): - """Initialize LevelListener.""" - super().__init__(cluster, device) - self.name = LISTENER_ZONE - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - if command_id == 0: - state = args[0] & 3 - async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - state - ) - _LOGGER.debug("Updated alarm state: %s", state) - elif command_id == 1: - _LOGGER.debug("Enroll requested") - res = self._cluster.enroll_response(0, 0) - self._zha_device.hass.async_create_task(res) - - async def async_configure(self): - """Configure IAS device.""" - from zigpy.exceptions import DeliveryError - _LOGGER.debug("%s: started IASZoneListener configuration", - self._unique_id) - - await bind_cluster(self.unique_id, self._cluster) - ieee = self._cluster.endpoint.device.application.ieee - - try: - res = await self._cluster.write_attributes({'cie_addr': ieee}) - _LOGGER.debug( - "%s: wrote cie_addr: %s to '%s' cluster: %s", - self.unique_id, str(ieee), self._cluster.ep_attribute, - res[0] - ) - except DeliveryError as ex: - _LOGGER.debug( - "%s: Failed to write cie_addr: %s to '%s' cluster: %s", - self.unique_id, str(ieee), self._cluster.ep_attribute, str(ex) - ) - _LOGGER.debug("%s: finished IASZoneListener configuration", - self._unique_id) - - await self.get_attribute_value('zone_type', from_cache=False) - - @callback - def attribute_updated(self, attrid, value): - """Handle attribute updates on this cluster.""" - if attrid == 2: - value = value & 3 - async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value - ) - - async def async_initialize(self, from_cache): - """Initialize listener.""" - await self.get_attribute_value('zone_status', from_cache=from_cache) - await self.get_attribute_value('zone_state', from_cache=from_cache) - await super().async_initialize(from_cache) - - -class ActivePowerListener(AttributeListener): - """Listener that polls active power level.""" - - def __init__(self, cluster, device): - """Initialize ActivePowerListener.""" - super().__init__(cluster, device) - self.name = LISTENER_ACTIVE_POWER - - async def async_update(self): - """Retrieve latest state.""" - _LOGGER.debug("%s async_update", self.unique_id) - - # This is a polling listener. Don't allow cache. - result = await self.get_attribute_value( - LISTENER_ACTIVE_POWER, from_cache=False) - async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - result - ) - - async def async_initialize(self, from_cache): - """Initialize listener.""" - await self.get_attribute_value( - LISTENER_ACTIVE_POWER, from_cache=from_cache) - await super().async_initialize(from_cache) - - -class BasicListener(ClusterListener): - """Listener to interact with the basic cluster.""" - - BATTERY = 3 - POWER_SOURCES = { - 0: 'Unknown', - 1: 'Mains (single phase)', - 2: 'Mains (3 phase)', - BATTERY: 'Battery', - 4: 'DC source', - 5: 'Emergency mains constantly powered', - 6: 'Emergency mains and transfer switch' - } - - def __init__(self, cluster, device): - """Initialize BasicListener.""" - super().__init__(cluster, device) - self.name = LISTENER_BASIC - self._power_source = None - - async def async_configure(self): - """Configure this listener.""" - await super().async_configure() - await self.async_initialize(False) - - async def async_initialize(self, from_cache): - """Initialize listener.""" - self._power_source = await self.get_attribute_value( - 'power_source', from_cache=from_cache) - await super().async_initialize(from_cache) - - def get_power_source(self): - """Get the power source.""" - return self._power_source - - -class BatteryListener(ClusterListener): - """Listener that polls active power level.""" - - def __init__(self, cluster, device): - """Initialize BatteryListener.""" - super().__init__(cluster, device) - self.name = LISTENER_BATTERY - - @callback - def attribute_updated(self, attrid, value): - """Handle attribute updates on this cluster.""" - attr = self._report_config[1].get('attr') - if isinstance(attr, str): - attr_id = get_attr_id_by_name(self.cluster, attr) - else: - attr_id = attr - if attrid == attr_id: - async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_STATE_ATTR), - 'battery_level', - value - ) - - async def async_initialize(self, from_cache): - """Initialize listener.""" - await self.async_read_state(from_cache) - await super().async_initialize(from_cache) - - async def async_update(self): - """Retrieve latest state.""" - await self.async_read_state(True) - - async def async_read_state(self, from_cache): - """Read data from the cluster.""" - await self.get_attribute_value( - 'battery_size', from_cache=from_cache) - await self.get_attribute_value( - 'battery_percentage_remaining', from_cache=from_cache) - await self.get_attribute_value( - 'active_power', from_cache=from_cache) - - -class EventRelayListener(ClusterListener): - """Event relay that can be attached to zigbee clusters.""" - - def __init__(self, cluster, device): - """Initialize EventRelayListener.""" - super().__init__(cluster, device) - self.name = LISTENER_EVENT_RELAY - - @callback - def attribute_updated(self, attrid, value): - """Handle an attribute updated on this cluster.""" - self.zha_send_event( - self._cluster, - SIGNAL_ATTR_UPDATED, - { - 'attribute_id': attrid, - 'attribute_name': self._cluster.attributes.get( - attrid, - ['Unknown'])[0], - 'value': value - } - ) - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle a cluster command received on this cluster.""" - if self._cluster.server_commands is not None and \ - self._cluster.server_commands.get(command_id) is not None: - self.zha_send_event( - self._cluster, - self._cluster.server_commands.get(command_id)[0], - args - ) - - -class ColorListener(ClusterListener): - """Color listener.""" - - CAPABILITIES_COLOR_XY = 0x08 - CAPABILITIES_COLOR_TEMP = 0x10 - UNSUPPORTED_ATTRIBUTE = 0x86 - - def __init__(self, cluster, device): - """Initialize ColorListener.""" - super().__init__(cluster, device) - self.name = LISTENER_COLOR - self._color_capabilities = None - - def get_color_capabilities(self): - """Return the color capabilities.""" - return self._color_capabilities - - async def async_initialize(self, from_cache): - """Initialize listener.""" - capabilities = await self.get_attribute_value( - 'color_capabilities', from_cache=from_cache) - - if capabilities is None: - # ZCL Version 4 devices don't support the color_capabilities - # attribute. In this version XY support is mandatory, but we - # need to probe to determine if the device supports color - # temperature. - capabilities = self.CAPABILITIES_COLOR_XY - result = await self.get_attribute_value( - 'color_temperature', from_cache=from_cache) - - if result is not self.UNSUPPORTED_ATTRIBUTE: - capabilities |= self.CAPABILITIES_COLOR_TEMP - self._color_capabilities = capabilities - await super().async_initialize(from_cache) - - -class FanListener(ClusterListener): - """Fan listener.""" - - _value_attribute = 0 - - def __init__(self, cluster, device): - """Initialize FanListener.""" - super().__init__(cluster, device) - self.name = LISTENER_FAN - - async def async_set_speed(self, value) -> None: - """Set the speed of the fan.""" - from zigpy.exceptions import DeliveryError - try: - await self.cluster.write_attributes({'fan_mode': value}) - except DeliveryError as ex: - _LOGGER.error("%s: Could not set speed: %s", self.unique_id, ex) - return - - async def async_update(self): - """Retrieve latest state.""" - result = await self.get_attribute_value('fan_mode', from_cache=True) - - async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - result - ) - - def attribute_updated(self, attrid, value): - """Handle attribute update from fan cluster.""" - attr_name = self.cluster.attributes.get(attrid, [attrid])[0] - _LOGGER.debug("%s: Attribute report '%s'[%s] = %s", - self.unique_id, self.cluster.name, attr_name, value) - if attrid == self._value_attribute: - async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value - ) - - async def async_initialize(self, from_cache): - """Initialize listener.""" - await self.get_attribute_value( - self._value_attribute, from_cache=from_cache) - await super().async_initialize(from_cache) - - -class ZDOListener: - """Listener for ZDO events.""" - - def __init__(self, cluster, device): - """Initialize ZDOListener.""" - self.name = 'zdo' - self._cluster = cluster - self._zha_device = device - self._status = ListenerStatus.CREATED - self._unique_id = "{}_ZDO".format(device.name) - self._cluster.add_listener(self) - - @property - def unique_id(self): - """Return the unique id for this listener.""" - return self._unique_id - - @property - def cluster(self): - """Return the aigpy cluster for this listener.""" - return self._cluster - - @property - def status(self): - """Return the status of the listener.""" - return self._status - - @callback - def device_announce(self, zigpy_device): - """Device announce handler.""" - pass - - @callback - def permit_duration(self, duration): - """Permit handler.""" - pass - - async def async_initialize(self, from_cache): - """Initialize listener.""" - self._status = ListenerStatus.INITIALIZED - - async def async_configure(self): - """Configure listener.""" - self._status = ListenerStatus.CONFIGURED diff --git a/homeassistant/components/zha/device_entity.py b/homeassistant/components/zha/device_entity.py index e8b765a07a6..5632c849d59 100644 --- a/homeassistant/components/zha/device_entity.py +++ b/homeassistant/components/zha/device_entity.py @@ -11,7 +11,7 @@ import time from homeassistant.core import callback from homeassistant.util import slugify from .entity import ZhaEntity -from .const import LISTENER_BATTERY, SIGNAL_STATE_ATTR +from .const import POWER_CONFIGURATION_CHANNEL, SIGNAL_STATE_ATTR _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ STATE_OFFLINE = 'offline' class ZhaDeviceEntity(ZhaEntity): """A base class for ZHA devices.""" - def __init__(self, zha_device, listeners, keepalive_interval=7200, + def __init__(self, zha_device, channels, keepalive_interval=7200, **kwargs): """Init ZHA endpoint entity.""" ieee = zha_device.ieee @@ -55,7 +55,7 @@ class ZhaDeviceEntity(ZhaEntity): unique_id = str(ieeetail) kwargs['component'] = 'zha' - super().__init__(unique_id, zha_device, listeners, skip_entity_id=True, + super().__init__(unique_id, zha_device, channels, skip_entity_id=True, **kwargs) self._keepalive_interval = keepalive_interval @@ -66,7 +66,8 @@ class ZhaDeviceEntity(ZhaEntity): 'rssi': zha_device.rssi, }) self._should_poll = True - self._battery_listener = self.cluster_listeners.get(LISTENER_BATTERY) + self._battery_channel = self.cluster_channels.get( + POWER_CONFIGURATION_CHANNEL) @property def state(self) -> str: @@ -97,9 +98,9 @@ class ZhaDeviceEntity(ZhaEntity): async def async_added_to_hass(self): """Run when about to be added to hass.""" await super().async_added_to_hass() - if self._battery_listener: + if self._battery_channel: await self.async_accept_signal( - self._battery_listener, SIGNAL_STATE_ATTR, + self._battery_channel, SIGNAL_STATE_ATTR, self.async_update_state_attribute) # only do this on add to HA because it is static await self._async_init_battery_values() @@ -114,7 +115,7 @@ class ZhaDeviceEntity(ZhaEntity): self._zha_device.update_available(False) else: self._zha_device.update_available(True) - if self._battery_listener: + if self._battery_channel: await self.async_get_latest_battery_reading() @callback @@ -127,14 +128,14 @@ class ZhaDeviceEntity(ZhaEntity): super().async_set_available(available) async def _async_init_battery_values(self): - """Get initial battery level and battery info from listener cache.""" - battery_size = await self._battery_listener.get_attribute_value( + """Get initial battery level and battery info from channel cache.""" + battery_size = await self._battery_channel.get_attribute_value( 'battery_size') if battery_size is not None: self._device_state_attributes['battery_size'] = BATTERY_SIZES.get( battery_size, 'Unknown') - battery_quantity = await self._battery_listener.get_attribute_value( + battery_quantity = await self._battery_channel.get_attribute_value( 'battery_quantity') if battery_quantity is not None: self._device_state_attributes['battery_quantity'] = \ @@ -142,8 +143,8 @@ class ZhaDeviceEntity(ZhaEntity): await self.async_get_latest_battery_reading() async def async_get_latest_battery_reading(self): - """Get the latest battery reading from listeners cache.""" - battery = await self._battery_listener.get_attribute_value( + """Get the latest battery reading from channels cache.""" + battery = await self._battery_channel.get_attribute_value( 'battery_percentage_remaining') if battery is not None: self._device_state_attributes['battery_level'] = battery diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index d914a76c4ce..2f5aed4ca29 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -27,7 +27,7 @@ class ZhaEntity(entity.Entity): _domain = None # Must be overridden by subclasses - def __init__(self, unique_id, zha_device, listeners, + def __init__(self, unique_id, zha_device, channels, skip_entity_id=False, **kwargs): """Init ZHA entity.""" self._force_update = False @@ -48,25 +48,25 @@ class ZhaEntity(entity.Entity): slugify(zha_device.manufacturer), slugify(zha_device.model), ieeetail, - listeners[0].cluster.endpoint.endpoint_id, + channels[0].cluster.endpoint.endpoint_id, kwargs.get(ENTITY_SUFFIX, ''), ) else: self.entity_id = "{}.zha_{}_{}{}".format( self._domain, ieeetail, - listeners[0].cluster.endpoint.endpoint_id, + channels[0].cluster.endpoint.endpoint_id, kwargs.get(ENTITY_SUFFIX, ''), ) self._state = None self._device_state_attributes = {} self._zha_device = zha_device - self.cluster_listeners = {} + self.cluster_channels = {} self._available = False self._component = kwargs['component'] self._unsubs = [] - for listener in listeners: - self.cluster_listeners[listener.name] = listener + for channel in channels: + self.cluster_channels[channel.name] = channel @property def name(self): @@ -147,7 +147,7 @@ class ZhaEntity(entity.Entity): ) self._zha_device.gateway.register_entity_reference( self._zha_device.ieee, self.entity_id, self._zha_device, - self.cluster_listeners, self.device_info) + self.cluster_channels, self.device_info) async def async_will_remove_from_hass(self) -> None: """Disconnect entity object when removed.""" @@ -156,13 +156,13 @@ class ZhaEntity(entity.Entity): async def async_update(self): """Retrieve latest state.""" - for listener in self.cluster_listeners: - if hasattr(listener, 'async_update'): - await listener.async_update() + for channel in self.cluster_channels: + if hasattr(channel, 'async_update'): + await channel.async_update() - async def async_accept_signal(self, listener, signal, func, + async def async_accept_signal(self, channel, signal, func, signal_override=False): - """Accept a signal from a listener.""" + """Accept a signal from a channel.""" unsub = None if signal_override: unsub = async_dispatcher_connect( @@ -173,7 +173,7 @@ class ZhaEntity(entity.Entity): else: unsub = async_dispatcher_connect( self.hass, - "{}_{}".format(listener.unique_id, signal), + "{}_{}".format(channel.unique_id, signal), func ) self._unsubs.append(unsub) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index dfe3c8cdd23..761dfaede1e 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -11,7 +11,7 @@ from homeassistant.components.fan import ( FanEntity) from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_FAN, + DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, FAN_CHANNEL, SIGNAL_ATTR_UPDATED ) from .entity import ZhaEntity @@ -81,16 +81,16 @@ class ZhaFan(ZhaEntity, FanEntity): _domain = DOMAIN - def __init__(self, unique_id, zha_device, listeners, **kwargs): + def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" - super().__init__(unique_id, zha_device, listeners, **kwargs) - self._fan_listener = self.cluster_listeners.get(LISTENER_FAN) + super().__init__(unique_id, zha_device, channels, **kwargs) + self._fan_channel = self.cluster_channels.get(FAN_CHANNEL) async def async_added_to_hass(self): """Run when about to be added to hass.""" await super().async_added_to_hass() await self.async_accept_signal( - self._fan_listener, SIGNAL_ATTR_UPDATED, self.async_set_state) + self._fan_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) @property def supported_features(self) -> int: @@ -120,7 +120,7 @@ class ZhaFan(ZhaEntity, FanEntity): return self.state_attributes def async_set_state(self, state): - """Handle state update from listener.""" + """Handle state update from channel.""" self._state = VALUE_TO_SPEED.get(state, self._state) self.async_schedule_update_ha_state() @@ -137,5 +137,5 @@ class ZhaFan(ZhaEntity, FanEntity): async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" - await self._fan_listener.async_set_speed(SPEED_TO_VALUE[speed]) + await self._fan_channel.async_set_speed(SPEED_TO_VALUE[speed]) self.async_set_state(speed) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 09f1812cd76..efa6f679ae8 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -10,8 +10,8 @@ from homeassistant.components import light from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util from .const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_COLOR, - LISTENER_ON_OFF, LISTENER_LEVEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL + DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, COLOR_CHANNEL, + ON_OFF_CHANNEL, LEVEL_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL ) from .entity import ZhaEntity @@ -67,24 +67,24 @@ class Light(ZhaEntity, light.Light): _domain = light.DOMAIN - def __init__(self, unique_id, zha_device, listeners, **kwargs): + def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize the ZHA light.""" - super().__init__(unique_id, zha_device, listeners, **kwargs) + super().__init__(unique_id, zha_device, channels, **kwargs) self._supported_features = 0 self._color_temp = None self._hs_color = None self._brightness = None - self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF) - self._level_listener = self.cluster_listeners.get(LISTENER_LEVEL) - self._color_listener = self.cluster_listeners.get(LISTENER_COLOR) + self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) + self._level_channel = self.cluster_channels.get(LEVEL_CHANNEL) + self._color_channel = self.cluster_channels.get(COLOR_CHANNEL) - if self._level_listener: + if self._level_channel: self._supported_features |= light.SUPPORT_BRIGHTNESS self._supported_features |= light.SUPPORT_TRANSITION self._brightness = 0 - if self._color_listener: - color_capabilities = self._color_listener.get_color_capabilities() + if self._color_channel: + color_capabilities = self._color_channel.get_color_capabilities() if color_capabilities & CAPABILITIES_COLOR_TEMP: self._supported_features |= light.SUPPORT_COLOR_TEMP @@ -139,10 +139,10 @@ class Light(ZhaEntity, light.Light): """Run when about to be added to hass.""" await super().async_added_to_hass() await self.async_accept_signal( - self._on_off_listener, SIGNAL_ATTR_UPDATED, self.async_set_state) - if self._level_listener: + self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) + if self._level_channel: await self.async_accept_signal( - self._level_listener, SIGNAL_SET_LEVEL, self.set_level) + self._level_channel, SIGNAL_SET_LEVEL, self.set_level) async def async_turn_on(self, **kwargs): """Turn the entity on.""" @@ -152,7 +152,7 @@ class Light(ZhaEntity, light.Light): if light.ATTR_COLOR_TEMP in kwargs and \ self.supported_features & light.SUPPORT_COLOR_TEMP: temperature = kwargs[light.ATTR_COLOR_TEMP] - success = await self._color_listener.move_to_color_temp( + success = await self._color_channel.move_to_color_temp( temperature, duration) if not success: return @@ -162,7 +162,7 @@ class Light(ZhaEntity, light.Light): self.supported_features & light.SUPPORT_COLOR: hs_color = kwargs[light.ATTR_HS_COLOR] xy_color = color_util.color_hs_to_xy(*hs_color) - success = await self._color_listener.move_to_color( + success = await self._color_channel.move_to_color( int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration, @@ -174,7 +174,7 @@ class Light(ZhaEntity, light.Light): if self._brightness is not None: brightness = kwargs.get( light.ATTR_BRIGHTNESS, self._brightness or 255) - success = await self._level_listener.move_to_level_with_on_off( + success = await self._level_channel.move_to_level_with_on_off( brightness, duration ) @@ -185,7 +185,7 @@ class Light(ZhaEntity, light.Light): self.async_schedule_update_ha_state() return - success = await self._on_off_listener.on() + success = await self._on_off_channel.on() if not success: return @@ -198,12 +198,12 @@ class Light(ZhaEntity, light.Light): supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS success = None if duration and supports_level: - success = await self._level_listener.move_to_level_with_on_off( + success = await self._level_channel.move_to_level_with_on_off( 0, duration*10 ) else: - success = await self._on_off_listener.off() + success = await self._on_off_channel.off() _LOGGER.debug("%s was turned off: %s", self.entity_id, success) if not success: return diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 9c00d8124bb..6dcdbb845dc 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -12,7 +12,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT, - GENERIC, SENSOR_TYPE, LISTENER_ATTRIBUTE, LISTENER_ACTIVE_POWER, + GENERIC, SENSOR_TYPE, ATTRIBUTE_CHANNEL, ELECTRICAL_MEASUREMENT_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR) from .entity import ZhaEntity @@ -74,8 +74,8 @@ UNIT_REGISTRY = { GENERIC: None } -LISTENER_REGISTRY = { - ELECTRICAL_MEASUREMENT: LISTENER_ACTIVE_POWER, +CHANNEL_REGISTRY = { + ELECTRICAL_MEASUREMENT: ELECTRICAL_MEASUREMENT_CHANNEL, } POLLING_REGISTRY = { @@ -130,9 +130,9 @@ class Sensor(ZhaEntity): _domain = DOMAIN - def __init__(self, unique_id, zha_device, listeners, **kwargs): + def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" - super().__init__(unique_id, zha_device, listeners, **kwargs) + super().__init__(unique_id, zha_device, channels, **kwargs) sensor_type = kwargs.get(SENSOR_TYPE, GENERIC) self._unit = UNIT_REGISTRY.get(sensor_type) self._formatter_function = FORMATTER_FUNC_REGISTRY.get( @@ -147,17 +147,17 @@ class Sensor(ZhaEntity): sensor_type, False ) - self._listener = self.cluster_listeners.get( - LISTENER_REGISTRY.get(sensor_type, LISTENER_ATTRIBUTE) + self._channel = self.cluster_channels.get( + CHANNEL_REGISTRY.get(sensor_type, ATTRIBUTE_CHANNEL) ) async def async_added_to_hass(self): """Run when about to be added to hass.""" await super().async_added_to_hass() await self.async_accept_signal( - self._listener, SIGNAL_ATTR_UPDATED, self.async_set_state) + self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state) await self.async_accept_signal( - self._listener, SIGNAL_STATE_ATTR, + self._channel, SIGNAL_STATE_ATTR, self.async_update_state_attribute) @property @@ -175,6 +175,6 @@ class Sensor(ZhaEntity): return self._state def async_set_state(self, state): - """Handle state update from listener.""" + """Handle state update from channel.""" self._state = self._formatter_function(state) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 4eee3d5da35..bdbdd7a6a76 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( - DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, LISTENER_ON_OFF, + DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, ON_OFF_CHANNEL, SIGNAL_ATTR_UPDATED ) from .entity import ZhaEntity @@ -60,7 +60,7 @@ class Switch(ZhaEntity, SwitchDevice): def __init__(self, **kwargs): """Initialize the ZHA switch.""" super().__init__(**kwargs) - self._on_off_listener = self.cluster_listeners.get(LISTENER_ON_OFF) + self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) @property def is_on(self) -> bool: @@ -71,14 +71,14 @@ class Switch(ZhaEntity, SwitchDevice): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - await self._on_off_listener.on() + await self._on_off_channel.on() async def async_turn_off(self, **kwargs): """Turn the entity off.""" - await self._on_off_listener.off() + await self._on_off_channel.off() def async_set_state(self, state): - """Handle state update from listener.""" + """Handle state update from channel.""" self._state = bool(state) self.async_schedule_update_ha_state() @@ -91,4 +91,4 @@ class Switch(ZhaEntity, SwitchDevice): """Run when about to be added to hass.""" await super().async_added_to_hass() await self.async_accept_signal( - self._on_off_listener, SIGNAL_ATTR_UPDATED, self.async_set_state) + self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index c806b1a2217..bd594941da1 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -7,8 +7,8 @@ from homeassistant.components.zha.core.const import ( ) from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.gateway import establish_device_mappings -from homeassistant.components.zha.core.listeners \ - import populate_listener_registry +from homeassistant.components.zha.core.channels.registry \ + import populate_channel_registry from .common import async_setup_entry @@ -28,7 +28,7 @@ def zha_gateway_fixture(hass): Create a ZHAGateway object that can be used to interact with as if we had a real zigbee network running. """ - populate_listener_registry() + populate_channel_registry() establish_device_mappings() for component in COMPONENTS: hass.data[DATA_ZHA][component] = ( From fb820975b534a9d8721d4e1c01d0614ad6ee6559 Mon Sep 17 00:00:00 2001 From: zewelor Date: Tue, 19 Feb 2019 19:06:40 +0100 Subject: [PATCH 080/253] Add yeelight flow action support (#21195) --- homeassistant/components/light/services.yaml | 3 +++ homeassistant/components/light/yeelight.py | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 9836bf97f90..10cbeb42aa4 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -202,6 +202,9 @@ yeelight_start_flow: count: description: The number of times to run this flow (0 to run forever). example: 0 + action: + description: The action to take after the flow stops. Can be 'recover', 'stay', 'off'. (default 'recover') + example: 'stay' transitions: description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index b678fcd2799..d22d94f7bbc 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -46,8 +46,13 @@ DATA_KEY = 'light.yeelight' ATTR_MODE = 'mode' ATTR_COUNT = 'count' +ATTR_ACTION = 'action' ATTR_TRANSITIONS = 'transitions' +ACTION_RECOVER = 'recover' +ACTION_STAY = 'stay' +ACTION_OFF = 'off' + YEELIGHT_RGB_TRANSITION = 'RGBTransition' YEELIGHT_HSV_TRANSACTION = 'HSVTransition' YEELIGHT_TEMPERATURE_TRANSACTION = 'TemperatureTransition' @@ -59,6 +64,8 @@ YEELIGHT_SERVICE_SCHEMA = vol.Schema({ YEELIGHT_FLOW_TRANSITION_SCHEMA = { vol.Optional(ATTR_COUNT, default=0): cv.positive_int, + vol.Optional(ATTR_ACTION, default=ACTION_RECOVER): + vol.Any(ACTION_RECOVER, ACTION_OFF, ACTION_STAY), vol.Required(ATTR_TRANSITIONS): [{ vol.Exclusive(YEELIGHT_RGB_TRANSITION, CONF_TRANSITION): vol.All(cv.ensure_list, [cv.positive_int]), @@ -604,13 +611,14 @@ class YeelightLight(Light): return transition_objects - def start_flow(self, transitions, count=0): + def start_flow(self, transitions, count=0, action=ACTION_RECOVER): """Start flow.""" import yeelight try: flow = yeelight.Flow( count=count, + action=yeelight.Flow.actions[action], transitions=self.transitions_config_parser(transitions)) self._bulb.start_flow(flow) From f452409cfacbb36f7b07f56afa696c464ce53b73 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Feb 2019 10:14:33 -0800 Subject: [PATCH 081/253] Updated frontend to 20190219.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 3b1d961ebe7..dce5b78bb6d 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190218.0'] +REQUIREMENTS = ['home-assistant-frontend==20190219.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index a380688fa86..25205e19a55 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190218.0 +home-assistant-frontend==20190219.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67eda186468..04bc61f41a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -116,7 +116,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190218.0 +home-assistant-frontend==20190219.0 # homeassistant.components.homekit_controller homekit==0.12.2 From 49995c21207973cf989e6cfe5f9f5cba51f54e7c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Feb 2019 13:52:14 -0800 Subject: [PATCH 082/253] Fix the build (#21229) --- homeassistant/package_constraints.txt | 3 +++ script/gen_requirements_all.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 775425eb58b..82efcb56295 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,3 +26,6 @@ pycrypto==1000000000.0.0 # Contains code to modify Home Assistant to work around our rules python-systemair-savecair==1000000000.0.0 + +# The 2019.02.19 failed to be imported, breaking our builds +regex==2019.02.07 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 46f111ded6c..c1f6af940fc 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -158,6 +158,9 @@ pycrypto==1000000000.0.0 # Contains code to modify Home Assistant to work around our rules python-systemair-savecair==1000000000.0.0 + +# The 2019.02.19 failed to be imported, breaking our builds +regex==2019.02.07 """ From 27d598fff86f1efcdc772671816cf37d609695dd Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Wed, 20 Feb 2019 00:31:46 +0100 Subject: [PATCH 083/253] Update pyhomematic to 0.1.56 (#21227) --- homeassistant/components/homematic/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index e71bc1d8f2d..4a27d918d95 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyhomematic==0.1.55'] +REQUIREMENTS = ['pyhomematic==0.1.56'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 25205e19a55..37b86f24ced 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1062,7 +1062,7 @@ pyhik==0.2.2 pyhiveapi==0.2.17 # homeassistant.components.homematic -pyhomematic==0.1.55 +pyhomematic==0.1.56 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04bc61f41a3..5dc713d89cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -190,7 +190,7 @@ pydeconz==52 pydispatcher==2.0.5 # homeassistant.components.homematic -pyhomematic==0.1.55 +pyhomematic==0.1.56 # homeassistant.components.litejet pylitejet==0.1 From cf3a8b60ffa00eb144d56a8fac73f3b38c5bf655 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Feb 2019 23:02:56 -0800 Subject: [PATCH 084/253] Prevent invalid context from crashing (#21231) * Prevent invalid context from crashing * Lint --- homeassistant/core.py | 5 +- tests/test_core.py | 105 ++++++++++++++++++++++++------------------ 2 files changed, 64 insertions(+), 46 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index c181ad453f3..48ef4f46272 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -744,7 +744,10 @@ class State: context = json_dict.get('context') if context: - context = Context(**context) + context = Context( + id=context.get('id'), + user_id=context.get('user_id'), + ) return cls(json_dict['entity_id'], json_dict['state'], json_dict.get('attributes'), last_changed, last_updated, diff --git a/tests/test_core.py b/tests/test_core.py index 818c6e4c087..e2ed249f441 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -460,61 +460,76 @@ class TestEventBus(unittest.TestCase): assert len(coroutine_calls) == 1 -class TestState(unittest.TestCase): - """Test State methods.""" +def test_state_init(): + """Test state.init.""" + with pytest.raises(InvalidEntityFormatError): + ha.State('invalid_entity_format', 'test_state') - def test_init(self): - """Test state.init.""" - with pytest.raises(InvalidEntityFormatError): - ha.State('invalid_entity_format', 'test_state') + with pytest.raises(InvalidStateError): + ha.State('domain.long_state', 't' * 256) - with pytest.raises(InvalidStateError): - ha.State('domain.long_state', 't' * 256) - def test_domain(self): - """Test domain.""" - state = ha.State('some_domain.hello', 'world') - assert 'some_domain' == state.domain +def test_state_domain(): + """Test domain.""" + state = ha.State('some_domain.hello', 'world') + assert 'some_domain' == state.domain - def test_object_id(self): - """Test object ID.""" - state = ha.State('domain.hello', 'world') - assert 'hello' == state.object_id - def test_name_if_no_friendly_name_attr(self): - """Test if there is no friendly name.""" - state = ha.State('domain.hello_world', 'world') - assert 'hello world' == state.name +def test_state_object_id(): + """Test object ID.""" + state = ha.State('domain.hello', 'world') + assert 'hello' == state.object_id - def test_name_if_friendly_name_attr(self): - """Test if there is a friendly name.""" - name = 'Some Unique Name' - state = ha.State('domain.hello_world', 'world', - {ATTR_FRIENDLY_NAME: name}) - assert name == state.name - def test_dict_conversion(self): - """Test conversion of dict.""" - state = ha.State('domain.hello', 'world', {'some': 'attr'}) - assert state == ha.State.from_dict(state.as_dict()) +def test_state_name_if_no_friendly_name_attr(): + """Test if there is no friendly name.""" + state = ha.State('domain.hello_world', 'world') + assert 'hello world' == state.name - def test_dict_conversion_with_wrong_data(self): - """Test conversion with wrong data.""" - assert ha.State.from_dict(None) is None - assert ha.State.from_dict({'state': 'yes'}) is None - assert ha.State.from_dict({'entity_id': 'yes'}) is None - def test_repr(self): - """Test state.repr.""" - assert "" == \ - str(ha.State( - "happy.happy", "on", - last_changed=datetime(1984, 12, 8, 12, 0, 0))) +def test_state_name_if_friendly_name_attr(): + """Test if there is a friendly name.""" + name = 'Some Unique Name' + state = ha.State('domain.hello_world', 'world', + {ATTR_FRIENDLY_NAME: name}) + assert name == state.name - assert "" == \ - str(ha.State("happy.happy", "on", {"brightness": 144}, - datetime(1984, 12, 8, 12, 0, 0))) + +def test_state_dict_conversion(): + """Test conversion of dict.""" + state = ha.State('domain.hello', 'world', {'some': 'attr'}) + assert state == ha.State.from_dict(state.as_dict()) + + +def test_state_dict_conversion_with_wrong_data(): + """Test conversion with wrong data.""" + assert ha.State.from_dict(None) is None + assert ha.State.from_dict({'state': 'yes'}) is None + assert ha.State.from_dict({'entity_id': 'yes'}) is None + # Make sure invalid context data doesn't crash + wrong_context = ha.State.from_dict({ + 'entity_id': 'light.kitchen', + 'state': 'on', + 'context': { + 'id': '123', + 'non-existing': 'crash' + } + }) + assert wrong_context is not None + assert wrong_context.context.id == '123' + + +def test_state_repr(): + """Test state.repr.""" + assert "" == \ + str(ha.State( + "happy.happy", "on", + last_changed=datetime(1984, 12, 8, 12, 0, 0))) + + assert "" == \ + str(ha.State("happy.happy", "on", {"brightness": 144}, + datetime(1984, 12, 8, 12, 0, 0))) class TestStateMachine(unittest.TestCase): From 1ff299875bc1dea7a20db7018cf3e9ec3bb0bf97 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 20 Feb 2019 07:34:10 +0000 Subject: [PATCH 085/253] Add self to integration sensor and utility_meter (#21226) --- CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 64c651752e7..fde408b84a8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -112,6 +112,7 @@ homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/gitter.py @fabaff homeassistant/components/sensor/glances.py @fabaff homeassistant/components/sensor/gpsd.py @fabaff +homeassistant/components/sensor/integration.py @dgomes homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/jewish_calendar.py @tsvi homeassistant/components/sensor/launch_library.py @ludeeus @@ -266,6 +267,7 @@ homeassistant/components/*/tradfri.py @ggravlingen homeassistant/components/unifi/* @kane610 homeassistant/components/switch/unifi.py @kane610 homeassistant/components/upcloud/* @scop +homeassistant/components/utility_meter/* @dgomes homeassistant/components/*/upcloud.py @scop # V From 7e06d03b45c56ebc2406faaa0d7c7def7b2f7f51 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Feb 2019 04:10:42 -0700 Subject: [PATCH 086/253] Fix an Ambient PWS exception when location info is missing (#21220) --- homeassistant/components/ambient_station/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 4a7864d3f7f..4464992e5fa 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -339,8 +339,10 @@ class AmbientStation: self.stations[station['macAddress']] = { ATTR_LAST_DATA: station['lastData'], - ATTR_LOCATION: station['info']['location'], - ATTR_NAME: station['info']['name'], + ATTR_LOCATION: station.get('info', {}).get('location'), + ATTR_NAME: + station.get('info', {}).get( + 'name', station['macAddress']), } for component in ('binary_sensor', 'sensor'): From 5115dfada2df8a4277c39b213ff1ca5eb761bd00 Mon Sep 17 00:00:00 2001 From: Marco Gazzola Date: Wed, 20 Feb 2019 15:44:04 +0100 Subject: [PATCH 087/253] Add zone and reps for Xiaomi vacuum (#19777) * xiaomi vacuum with zone and reps * tail whitespace * tail whitespaces * new version * fix params typs * fix param type * line length * rytilahti tips * houndci-bot * fix trevis * rytilahti tips * service description * syssi fix * MartinHjelmare tips * MartinHjelmare * data_template schema * line lenght * line lenght * line lenght * data_template schema * fix * Update homeassistant/components/vacuum/xiaomi_miio.py Co-Authored-By: marcogazzola * Update homeassistant/components/vacuum/xiaomi_miio.py Co-Authored-By: marcogazzola * xiaomi vacuum with zone and reps * tail whitespace * new version * fix param type * rytilahti tips * rytilahti tips * MartinHjelmare * data_template schema * line lenght * line lenght * data_template schema * fix * Merge branch 'dev' of https://github.com/marcogazzola/home-assistant into dev * Revert "Merge branch 'dev' of https://github.com/marcogazzola/home-assistant into dev" This reverts commit e1f370b3b45d2541c8117146b0940d7c2b5bc8b0. * log fixed * Revert "log fixed" This reverts commit 1f0e7b35e81154af476a0bf1ae00773180cadb98. * Revert "Revert "Merge branch 'dev' of https://github.com/marcogazzola/home-assistant into dev"" This reverts commit 1cf9e5ae1f5c1c0504fbf59763b39c6a5656cb91. * Revert "Merge branch 'dev' of https://github.com/marcogazzola/home-assistant into dev" This reverts commit 0e8d53449a98fd33756397f31a891dd0d4d206ce. * log fixed --- homeassistant/components/vacuum/services.yaml | 13 ++++++ .../components/xiaomi_miio/vacuum.py | 41 +++++++++++++++++++ tests/components/xiaomi_miio/test_vacuum.py | 13 +++++- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index 6e40b3d67fc..792658bbdfd 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -131,3 +131,16 @@ xiaomi_remote_control_move_step: duration: description: Duration of the movement. example: '1500' + +xiaomi_clean_zone: + description: Start the cleaning operation in the selected areas for the number of repeats indicated. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + zone: + description: Array of zones. Each zone is an array of 4 integer values. + example: '[[23510,25311,25110,26362]]' + repeats: + description: Number of cleaning repeats for each zone between 1 and 3. + example: '1' diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 82a92dd317c..36196158600 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -33,6 +33,7 @@ SERVICE_MOVE_REMOTE_CONTROL = 'xiaomi_remote_control_move' SERVICE_MOVE_REMOTE_CONTROL_STEP = 'xiaomi_remote_control_move_step' SERVICE_START_REMOTE_CONTROL = 'xiaomi_remote_control_start' SERVICE_STOP_REMOTE_CONTROL = 'xiaomi_remote_control_stop' +SERVICE_CLEAN_ZONE = 'xiaomi_clean_zone' FAN_SPEEDS = { 'Quiet': 38, @@ -58,6 +59,8 @@ ATTR_RC_DURATION = 'duration' ATTR_RC_ROTATION = 'rotation' ATTR_RC_VELOCITY = 'velocity' ATTR_STATUS = 'status' +ATTR_ZONE_ARRAY = 'zone' +ATTR_ZONE_REPEATER = 'repeats' SERVICE_SCHEMA_REMOTE_CONTROL = VACUUM_SERVICE_SCHEMA.extend({ vol.Optional(ATTR_RC_VELOCITY): @@ -67,6 +70,24 @@ SERVICE_SCHEMA_REMOTE_CONTROL = VACUUM_SERVICE_SCHEMA.extend({ vol.Optional(ATTR_RC_DURATION): cv.positive_int, }) +SERVICE_SCHEMA_CLEAN_ZONE = VACUUM_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_ZONE_ARRAY): + vol.All(list, [vol.ExactSequence( + [vol.Coerce(int), vol.Coerce(int), + vol.Coerce(int), vol.Coerce(int)])]), + vol.Required(ATTR_ZONE_REPEATER): + vol.All(vol.Coerce(int), vol.Clamp(min=1, max=3)), +}) + +SERVICE_SCHEMA_CLEAN_ZONE = VACUUM_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_ZONE_ARRAY): + vol.All(list, [vol.ExactSequence( + [vol.Coerce(int), vol.Coerce(int), + vol.Coerce(int), vol.Coerce(int)])]), + vol.Required(ATTR_ZONE_REPEATER): + vol.All(vol.Coerce(int), vol.Clamp(min=1, max=3)), +}) + SERVICE_TO_METHOD = { SERVICE_START_REMOTE_CONTROL: {'method': 'async_remote_control_start'}, SERVICE_STOP_REMOTE_CONTROL: {'method': 'async_remote_control_stop'}, @@ -76,6 +97,9 @@ SERVICE_TO_METHOD = { SERVICE_MOVE_REMOTE_CONTROL_STEP: { 'method': 'async_remote_control_move_step', 'schema': SERVICE_SCHEMA_REMOTE_CONTROL}, + SERVICE_CLEAN_ZONE: { + 'method': 'async_clean_zone', + 'schema': SERVICE_SCHEMA_CLEAN_ZONE}, } SUPPORT_XIAOMI = SUPPORT_STATE | SUPPORT_PAUSE | \ @@ -127,6 +151,7 @@ async def async_setup_platform(hass, config, async_add_entities, params = {key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID} entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: target_vacuums = [vac for vac in hass.data[DATA_KEY].values() if vac.entity_id in entity_ids] @@ -377,3 +402,19 @@ class MiroboVacuum(StateVacuumDevice): _LOGGER.error("Got OSError while fetching the state: %s", exc) except DeviceException as exc: _LOGGER.warning("Got exception while fetching the state: %s", exc) + + async def async_clean_zone(self, + zone, + repeats=1): + """Clean selected area for the number of repeats indicated.""" + from miio import DeviceException + for _zone in zone: + _zone.append(repeats) + _LOGGER.debug("Zone with repeats: %s", zone) + try: + await self.hass.async_add_executor_job( + self._vacuum.zoned_clean, zone) + except (OSError, DeviceException) as exc: + _LOGGER.error( + "Unable to send zoned_clean command to the vacuum: %s", + exc) diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index a1e937cb244..0bb557b1488 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -18,7 +18,8 @@ from homeassistant.components.xiaomi_miio.vacuum import ( ATTR_CLEANING_COUNT, ATTR_CLEANED_TOTAL_AREA, ATTR_CLEANING_TOTAL_TIME, CONF_HOST, CONF_NAME, CONF_TOKEN, SERVICE_MOVE_REMOTE_CONTROL, SERVICE_MOVE_REMOTE_CONTROL_STEP, - SERVICE_START_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL) + SERVICE_START_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL, + SERVICE_CLEAN_ZONE) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON) @@ -330,3 +331,13 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): [mock.call.Vacuum().manual_control_once(control_once)], any_order=True) mock_mirobo_is_on.assert_has_calls(status_calls, any_order=True) mock_mirobo_is_on.reset_mock() + + control = {"zone": [[123, 123, 123, 123]], "repeats": 2} + yield from hass.services.async_call( + DOMAIN, SERVICE_CLEAN_ZONE, + control, blocking=True) + mock_mirobo_is_off.assert_has_calls( + [mock.call.Vacuum().zoned_clean( + [[123, 123, 123, 123, 2]])], any_order=True) + mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) + mock_mirobo_is_off.reset_mock() From 5b24b271ccc244373d0e42976a6eb2c941112af8 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 20 Feb 2019 10:27:03 -0500 Subject: [PATCH 088/253] Don't dispatch to components when there are no channels for ZHA sensors (#21223) * don't dispatch when channels don't exist * review comment --- homeassistant/components/zha/core/gateway.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 4fbf96a22b6..cd549afc819 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -253,6 +253,10 @@ async def _create_cluster_channel(cluster, zha_device, is_new_join, async def _dispatch_discovery_info(hass, is_new_join, discovery_info): """Dispatch or store discovery information.""" + if not discovery_info['channels']: + _LOGGER.warning( + "there are no channels in the discovery info: %s", discovery_info) + return component = discovery_info['component'] if is_new_join: async_dispatcher_send( From cece6454e4688ad06dc23fbbbaccaaed1961ab3c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 20 Feb 2019 10:33:29 -0500 Subject: [PATCH 089/253] Fix bug in ZHA and tweak non sensor channel logic (#21234) * fix race condition and prevent profiles from stealing channels * fix battery voltage --- .../components/zha/core/channels/general.py | 2 +- homeassistant/components/zha/core/device.py | 10 +++++++++ homeassistant/components/zha/core/gateway.py | 21 +++++++++++-------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index bc015ae47f0..a29b23d340b 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -199,4 +199,4 @@ class PowerConfigurationChannel(ZigbeeChannel): await self.get_attribute_value( 'battery_percentage_remaining', from_cache=from_cache) await self.get_attribute_value( - 'active_power', from_cache=from_cache) + 'battery_voltage', from_cache=from_cache) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 3a012ed7895..12bb397fbc3 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import asyncio +from enum import Enum import logging from homeassistant.helpers.dispatcher import ( @@ -23,6 +24,13 @@ from .channels.general import BasicChannel _LOGGER = logging.getLogger(__name__) +class DeviceStatus(Enum): + """Status of a device.""" + + CREATED = 1 + INITIALIZED = 2 + + class ZHADevice: """ZHA Zigbee device object.""" @@ -61,6 +69,7 @@ class ZHADevice: self._zigpy_device.__class__.__name__ ) self.power_source = None + self.status = DeviceStatus.CREATED @property def name(self): @@ -186,6 +195,7 @@ class ZHADevice: self.name, BasicChannel.POWER_SOURCES.get(self.power_source) ) + self.status = DeviceStatus.INITIALIZED _LOGGER.debug('%s: completed initialization', self.name) async def _execute_channel_tasks(self, task_name, *args): diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index cd549afc819..a50bfeae1be 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -23,7 +23,7 @@ from .const import ( REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, SIGNAL_REMOVE, NO_SENSOR_CLUSTERS, POWER_CONFIGURATION_CHANNEL) -from .device import ZHADevice +from .device import ZHADevice, DeviceStatus from ..device_entity import ZhaDeviceEntity from .channels import ( AttributeListeningChannel, EventRelayChannel, ZDOChannel @@ -139,7 +139,9 @@ class ZHAGateway: """Update device that has just become available.""" if sender.ieee in self.devices: device = self.devices[sender.ieee] - device.update_available(True) + # avoid a race condition during new joins + if device.status is DeviceStatus.INITIALIZED: + device.update_available(True) async def async_device_initialized(self, device, is_new_join): """Handle device joined and basic information discovered (async).""" @@ -323,6 +325,14 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, cluster_match_tasks = [] event_channel_tasks = [] for cluster in endpoint.in_clusters.values(): + # don't let profiles prevent these channels from being created + if cluster.cluster_id in NO_SENSOR_CLUSTERS: + cluster_match_tasks.append(_handle_channel_only_cluster_match( + zha_device, + cluster, + is_new_join, + )) + if cluster.cluster_id not in profile_clusters[0]: cluster_match_tasks.append(_handle_single_cluster_match( hass, @@ -333,13 +343,6 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, is_new_join, )) - if cluster.cluster_id in NO_SENSOR_CLUSTERS: - cluster_match_tasks.append(_handle_channel_only_cluster_match( - zha_device, - cluster, - is_new_join, - )) - for cluster in endpoint.out_clusters.values(): if cluster.cluster_id not in profile_clusters[1]: cluster_match_tasks.append(_handle_single_cluster_match( From 1518a800697ea059d8a063330eda7946bbb7680a Mon Sep 17 00:00:00 2001 From: damarco Date: Wed, 20 Feb 2019 16:34:59 +0100 Subject: [PATCH 090/253] Bump zigpy (#21203) * Bump zigpy * Update requirements * Update test requirements * Bump zigpy-deconz --- homeassistant/components/zha/__init__.py | 8 ++++---- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 4 ++-- script/gen_requirements_all.py | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 6c7e83689ad..a53e5864552 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -29,11 +29,11 @@ from .core.gateway import establish_device_mappings from .core.channels.registry import populate_channel_registry REQUIREMENTS = [ - 'bellows==0.7.0', - 'zigpy==0.2.0', - 'zigpy-xbee==0.1.1', + 'bellows-homeassistant==0.7.1', + 'zigpy-homeassistant==0.3.0', + 'zigpy-xbee-homeassistant==0.1.2', 'zha-quirks==0.0.6', - 'zigpy-deconz==0.0.1' + 'zigpy-deconz==0.1.1' ] DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ diff --git a/requirements_all.txt b/requirements_all.txt index 37b86f24ced..63cb4b87072 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -196,7 +196,7 @@ batinfo==0.4.2 beautifulsoup4==4.7.1 # homeassistant.components.zha -bellows==0.7.0 +bellows-homeassistant==0.7.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.5.3 @@ -1811,13 +1811,13 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.0.1 +zigpy-deconz==0.1.1 # homeassistant.components.zha -zigpy-xbee==0.1.1 +zigpy-homeassistant==0.3.0 # homeassistant.components.zha -zigpy==0.2.0 +zigpy-xbee-homeassistant==0.1.2 # homeassistant.components.zoneminder zm-py==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5dc713d89cf..333233e76b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -50,7 +50,7 @@ aiounifi==4 apns2==0.3.0 # homeassistant.components.zha -bellows==0.7.0 +bellows-homeassistant==0.7.1 # homeassistant.components.calendar.caldav caldav==0.5.0 @@ -306,4 +306,4 @@ wakeonlan==1.1.6 warrant==0.6.1 # homeassistant.components.zha -zigpy==0.2.0 +zigpy-homeassistant==0.3.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index c1f6af940fc..b298a9704ca 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -125,8 +125,8 @@ TEST_REQUIREMENTS = ( 'vultr', 'YesssSMS', 'ruamel.yaml', - 'zigpy', - 'bellows', + 'zigpy-homeassistant', + 'bellows-homeassistant', ) IGNORE_PACKAGES = ( From 54949cff5ad3cc7aa43897055df7446ce3b69e9c Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Wed, 20 Feb 2019 15:55:00 +0000 Subject: [PATCH 091/253] Support OpenWRT 18.06 in luci device tracker (#21236) * Got it right this time i hope * updates on comments --- .../components/device_tracker/luci.py | 150 ++++-------------- requirements_all.txt | 3 + 2 files changed, 30 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 30b09834b68..27fbf7c9f44 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -4,21 +4,18 @@ Support for OpenWRT (luci) routers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.luci/ """ -import json -import logging -import re from collections import namedtuple - -import requests +import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.exceptions import HomeAssistantError from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import ( CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_SSL) +REQUIREMENTS = ['openwrt-luci-rpc==0.3.0'] + _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = False @@ -31,12 +28,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -class InvalidLuciTokenError(HomeAssistantError): - """When an invalid token is detected.""" - - pass - - def get_scanner(hass, config): """Validate the configuration and return a Luci scanner.""" scanner = LuciDeviceScanner(config[DOMAIN]) @@ -44,138 +35,51 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -Device = namedtuple('Device', ['mac', 'ip', 'flags', 'device', 'host']) +Device = namedtuple('Device', ['mac', 'name']) class LuciDeviceScanner(DeviceScanner): - """This class queries a wireless router running OpenWrt firmware.""" + """This class scans for devices connected to an OpenWrt router.""" def __init__(self, config): """Initialize the scanner.""" - self.host = config[CONF_HOST] + host = config[CONF_HOST] protocol = 'http' if not config[CONF_SSL] else 'https' - self.origin = '{}://{}'.format(protocol, self.host) - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] + host_url = '{}://{}'.format(protocol, host) - self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);") + from openwrt_luci_rpc import OpenWrtRpc + + self.router = OpenWrtRpc(host_url, + config[CONF_USERNAME], + config[CONF_PASSWORD]) self.last_results = {} - self.refresh_token() - self.mac2name = None - self.success_init = self.token is not None - - def refresh_token(self): - """Get a new token.""" - self.token = _get_token(self.origin, self.username, self.password) + self.success_init = self.router.is_logged_in() def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" self._update_info() + return [device.mac for device in self.last_results] def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - if self.mac2name is None: - url = '{}/cgi-bin/luci/rpc/uci'.format(self.origin) - result = _req_json_rpc( - url, 'get_all', 'dhcp', params={'auth': self.token}) - if result: - hosts = [x for x in result.values() - if x['.type'] == 'host' and - 'mac' in x and 'name' in x] - mac2name_list = [ - (x['mac'].upper(), x['name']) for x in hosts] - self.mac2name = dict(mac2name_list) - else: - # Error, handled in the _req_json_rpc - return - return self.mac2name.get(device.upper(), None) - - def get_extra_attributes(self, device): - """Return the IP of the given device.""" - filter_att = next(( - { - 'ip': result.ip, - 'flags': result.flags, - 'device': result.device, - 'host': result.host - } for result in self.last_results + name = next(( + result.name for result in self.last_results if result.mac == device), None) - return filter_att + return name def _update_info(self): - """Ensure the information from the Luci router is up to date. + """Check the Luci router for devices.""" + result = self.router.get_all_connected_devices( + only_reachable=True) - Returns boolean if scanning successful. - """ - if not self.success_init: - return False + _LOGGER.debug("Luci get_all_connected_devices returned:" + " %s", result) - _LOGGER.info("Checking ARP") + last_results = [] + for device in result: + last_results.append( + Device(device['macaddress'], device['hostname'])) - url = '{}/cgi-bin/luci/rpc/sys'.format(self.origin) - - try: - result = _req_json_rpc( - url, 'net.arptable', params={'auth': self.token}) - except InvalidLuciTokenError: - _LOGGER.info("Refreshing token") - self.refresh_token() - return False - - if result: - self.last_results = [] - for device_entry in result: - # Check if the Flags for each device contain - # NUD_REACHABLE and if so, add it to last_results - if int(device_entry['Flags'], 16) & 0x2: - self.last_results.append(Device(device_entry['HW address'], - device_entry['IP address'], - device_entry['Flags'], - device_entry['Device'], - self.host)) - - return True - - return False - - -def _req_json_rpc(url, method, *args, **kwargs): - """Perform one JSON RPC operation.""" - data = json.dumps({'method': method, 'params': args}) - - try: - res = requests.post(url, data=data, timeout=5, **kwargs) - except requests.exceptions.Timeout: - _LOGGER.exception("Connection to the router timed out") - return - if res.status_code == 200: - try: - result = res.json() - except ValueError: - # If json decoder could not parse the response - _LOGGER.exception("Failed to parse response from luci") - return - try: - return result['result'] - except KeyError: - _LOGGER.exception("No result in response from luci") - return - elif res.status_code == 401: - # Authentication error - _LOGGER.exception( - "Failed to authenticate, check your username and password") - return - elif res.status_code == 403: - _LOGGER.error("Luci responded with a 403 Invalid token") - raise InvalidLuciTokenError - - else: - _LOGGER.error("Invalid response from luci: %s", res) - - -def _get_token(origin, username, password): - """Get authentication token for the given configuration.""" - url = '{}/cgi-bin/luci/rpc/auth'.format(origin) - return _req_json_rpc(url, 'login', username, password) + self.last_results = last_results diff --git a/requirements_all.txt b/requirements_all.txt index 63cb4b87072..9e915f79417 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -775,6 +775,9 @@ openhomedevice==0.4.2 # homeassistant.components.air_quality.opensensemap opensensemap-api==0.1.3 +# homeassistant.components.device_tracker.luci +openwrt-luci-rpc==0.3.0 + # homeassistant.components.switch.orvibo orvibo==1.1.1 From 03573781c74f3e3dd98ea99048688c4b49d30fe8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Feb 2019 08:55:42 -0800 Subject: [PATCH 092/253] Updated frontend to 20190220.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 dce5b78bb6d..caf6bbccb5c 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190219.0'] +REQUIREMENTS = ['home-assistant-frontend==20190220.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 9e915f79417..ade1448e4c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190219.0 +home-assistant-frontend==20190220.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 333233e76b3..b2f16502ea8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -116,7 +116,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190219.0 +home-assistant-frontend==20190220.0 # homeassistant.components.homekit_controller homekit==0.12.2 From ddd63c615f9fae179e9b33d297c902186d43cf2d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Feb 2019 12:31:41 -0800 Subject: [PATCH 093/253] Remove constraint from regex (#21239) --- homeassistant/package_constraints.txt | 3 --- script/gen_requirements_all.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 82efcb56295..775425eb58b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,6 +26,3 @@ pycrypto==1000000000.0.0 # Contains code to modify Home Assistant to work around our rules python-systemair-savecair==1000000000.0.0 - -# The 2019.02.19 failed to be imported, breaking our builds -regex==2019.02.07 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index b298a9704ca..85d9c95aec7 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -158,9 +158,6 @@ pycrypto==1000000000.0.0 # Contains code to modify Home Assistant to work around our rules python-systemair-savecair==1000000000.0.0 - -# The 2019.02.19 failed to be imported, breaking our builds -regex==2019.02.07 """ From 966fd1034d63c682f6ff5e68d7b1d69eab6b34aa Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 21 Feb 2019 01:46:37 +0100 Subject: [PATCH 094/253] Upgrade opensensemap-api to 0.1.4 (#21240) --- homeassistant/components/air_quality/opensensemap.py | 9 ++------- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/air_quality/opensensemap.py b/homeassistant/components/air_quality/opensensemap.py index d77c0c9bfe2..8462e40be5b 100644 --- a/homeassistant/components/air_quality/opensensemap.py +++ b/homeassistant/components/air_quality/opensensemap.py @@ -1,9 +1,4 @@ -""" -Support for openSenseMap Air Quality data. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/air_quality.opensensemap/ -""" +"""Support for openSenseMap Air Quality data.""" from datetime import timedelta import logging @@ -16,7 +11,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['opensensemap-api==0.1.3'] +REQUIREMENTS = ['opensensemap-api==0.1.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ade1448e4c9..4e4e8b78962 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -773,7 +773,7 @@ openevsewifi==0.4 openhomedevice==0.4.2 # homeassistant.components.air_quality.opensensemap -opensensemap-api==0.1.3 +opensensemap-api==0.1.4 # homeassistant.components.device_tracker.luci openwrt-luci-rpc==0.3.0 From 73099caedefe2af759a35dfc07650b4cac183d0e Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Thu, 21 Feb 2019 06:50:02 +0100 Subject: [PATCH 095/253] Alarm trigger support for Point (#21207) --- .../components/point/alarm_control_panel.py | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index 10387fdc7c4..a50dffe42b9 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -1,16 +1,25 @@ """Support for Minut Point.""" import logging -from homeassistant.components.alarm_control_panel import (DOMAIN, - AlarmControlPanel) -from homeassistant.const import (STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED) +from homeassistant.components.alarm_control_panel import ( + DOMAIN, AlarmControlPanel) from homeassistant.components.point.const import ( - DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW) + DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) +EVENT_MAP = { + 'off': STATE_ALARM_DISARMED, + 'alarm_silenced': STATE_ALARM_ARMED_AWAY, + 'alarm_grace_period_expired': STATE_ALARM_TRIGGERED, +} + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Point's alarm_control_panel based on a config entry.""" async def async_discover_home(home_id): @@ -30,6 +39,32 @@ class MinutPointAlarmControl(AlarmControlPanel): """Initialize the entity.""" self._client = point_client self._home_id = home_id + self._async_unsub_hook_dispatcher_connect = None + self._changed_by = None + + async def async_added_to_hass(self): + """Call when entity is added to HOme Assistant.""" + await super().async_added_to_hass() + self._async_unsub_hook_dispatcher_connect = async_dispatcher_connect( + self.hass, SIGNAL_WEBHOOK, self._webhook_event) + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listener when removed.""" + await super().async_will_remove_from_hass() + if self._async_unsub_hook_dispatcher_connect: + self._async_unsub_hook_dispatcher_connect() + + @callback + def _webhook_event(self, data, webhook): + """Process new event from the webhook.""" + _type = data.get('event', {}).get('type') + _device_id = data.get('event', {}).get('device_id') + if _device_id not in self._home['devices'] or _type not in EVENT_MAP: + return + _LOGGER.debug("Recieved webhook: %s", _type) + self._home['alarm_status'] = EVENT_MAP[_type] + self._changed_by = _device_id + self.async_schedule_update_ha_state() @property def _home(self): @@ -44,8 +79,15 @@ class MinutPointAlarmControl(AlarmControlPanel): @property def state(self): """Return state of the device.""" - return STATE_ALARM_DISARMED if self._home[ - 'alarm_status'] == 'off' else STATE_ALARM_ARMED_AWAY + return EVENT_MAP.get( + self._home['alarm_status'], + STATE_ALARM_ARMED_AWAY, + ) + + @property + def changed_by(self): + """Return the user the last change was triggered by.""" + return self._changed_by def alarm_disarm(self, code=None): """Send disarm command.""" From 24354562488fe38e0c7f20bd0f41b2c039eb2144 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Feb 2019 00:41:36 -0800 Subject: [PATCH 096/253] Prevent partial custom component overlays (#21070) * Prevent partial custom component overlays * Fix tests --- homeassistant/loader.py | 51 +++++++++++++------ tests/common.py | 1 + tests/test_loader.py | 7 +++ .../custom_components/hue/comp_path_test.py | 1 + 4 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 tests/testing_config/custom_components/hue/comp_path_test.py diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 962b168aa97..50f8b4338d8 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -15,7 +15,7 @@ import importlib import logging import sys from types import ModuleType -from typing import Optional, Set, TYPE_CHECKING, Callable, Any, TypeVar # noqa pylint: disable=unused-import +from typing import Optional, Set, TYPE_CHECKING, Callable, Any, TypeVar, List # noqa pylint: disable=unused-import from homeassistant.const import PLATFORM_FORMAT @@ -34,8 +34,9 @@ _LOGGER = logging.getLogger(__name__) DATA_KEY = 'components' -PATH_CUSTOM_COMPONENTS = 'custom_components' -PACKAGE_COMPONENTS = 'homeassistant.components' +PACKAGE_CUSTOM_COMPONENTS = 'custom_components' +PACKAGE_BUILTIN = 'homeassistant.components' +LOOKUP_PATHS = [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] class LoaderError(Exception): @@ -76,23 +77,43 @@ def get_platform(hass, # type: HomeAssistant domain: str, platform_name: str) -> Optional[ModuleType]: """Try to load specified platform. + Example invocation: get_platform(hass, 'light', 'hue') + Async friendly. """ - platform = _load_file(hass, PLATFORM_FORMAT.format( - domain=domain, platform=platform_name)) + # If the platform has a component, we will limit the platform loading path + # to be the same source (custom/built-in). + component = get_component(hass, platform_name) + + # Until we have moved all platforms under their component/own folder, it + # can be that the component is None. + if component is not None: + base_paths = [component.__name__.rsplit('.', 1)[0]] + else: + base_paths = LOOKUP_PATHS + + platform = _load_file( + hass, PLATFORM_FORMAT.format(domain=domain, platform=platform_name), + base_paths) if platform is not None: return platform # Legacy platform check: light/hue.py - platform = _load_file(hass, PLATFORM_FORMAT.format( - domain=platform_name, platform=domain)) + platform = _load_file( + hass, PLATFORM_FORMAT.format(domain=platform_name, platform=domain), + base_paths) if platform is None: - _LOGGER.error("Unable to find platform %s", platform_name) + if component is None: + extra = "" + else: + extra = " Search path was limited to path of component: {}".format( + base_paths[0]) + _LOGGER.error("Unable to find platform %s.%s", platform_name, extra) return None - if platform.__name__.startswith(PATH_CUSTOM_COMPONENTS): + if platform.__name__.startswith(PACKAGE_CUSTOM_COMPONENTS): _LOGGER.warning( "Integrations need to be in their own folder. Change %s/%s.py to " "%s/%s.py. This will stop working soon.", @@ -107,7 +128,7 @@ def get_component(hass, # type: HomeAssistant Async friendly. """ - comp = _load_file(hass, comp_or_platform) + comp = _load_file(hass, comp_or_platform, LOOKUP_PATHS) if comp is None: _LOGGER.error("Unable to find component %s", comp_or_platform) @@ -116,7 +137,8 @@ def get_component(hass, # type: HomeAssistant def _load_file(hass, # type: HomeAssistant - comp_or_platform: str) -> Optional[ModuleType]: + comp_or_platform: str, + base_paths: List[str]) -> Optional[ModuleType]: """Try to load specified file. Looks in config dir first, then built-in components. @@ -138,11 +160,8 @@ def _load_file(hass, # type: HomeAssistant sys.path.insert(0, hass.config.config_dir) cache = hass.data[DATA_KEY] = {} - # First check custom, then built-in - potential_paths = ['custom_components.{}'.format(comp_or_platform), - 'homeassistant.components.{}'.format(comp_or_platform)] - - for index, path in enumerate(potential_paths): + for index, path in enumerate('{}.{}'.format(base, comp_or_platform) + for base in base_paths): try: module = importlib.import_module(path) diff --git a/tests/common.py b/tests/common.py index 28c6e4c5301..0c1d6854886 100644 --- a/tests/common.py +++ b/tests/common.py @@ -454,6 +454,7 @@ class MockModule: async_setup_entry=None, async_unload_entry=None, async_migrate_entry=None): """Initialize the mock module.""" + self.__name__ = 'homeassistant.components.{}'.format(domain) self.DOMAIN = domain self.DEPENDENCIES = dependencies or [] self.REQUIREMENTS = requirements or [] diff --git a/tests/test_loader.py b/tests/test_loader.py index cceb9839d99..09f830a8eab 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -135,3 +135,10 @@ async def test_get_platform(hass, caplog): legacy_platform = loader.get_platform(hass, 'switch', 'test') assert legacy_platform.__name__ == 'custom_components.switch.test' assert 'Integrations need to be in their own folder.' in caplog.text + + +async def test_get_platform_enforces_component_path(hass, caplog): + """Test that existence of a component limits lookup path of platforms.""" + assert loader.get_platform(hass, 'comp_path_test', 'hue') is None + assert ('Search path was limited to path of component: ' + 'homeassistant.components') in caplog.text diff --git a/tests/testing_config/custom_components/hue/comp_path_test.py b/tests/testing_config/custom_components/hue/comp_path_test.py new file mode 100644 index 00000000000..3214c58a44d --- /dev/null +++ b/tests/testing_config/custom_components/hue/comp_path_test.py @@ -0,0 +1 @@ +"""Custom platform for a built-in component, should not be allowed.""" From 0f8575f9392b33230ec6c2e0d6fe6f0e043edf8c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 21 Feb 2019 08:20:58 -0500 Subject: [PATCH 097/253] Fix ZHA bugs (#21246) * fix bugs * add comment * allow entities to be marked unavailable --- .../components/zha/core/channels/general.py | 4 +++- homeassistant/components/zha/core/device.py | 10 +++++----- homeassistant/components/zha/core/gateway.py | 13 +++++++++++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index a29b23d340b..621b0ccbee1 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -126,9 +126,11 @@ class LevelControlChannel(ZigbeeChannel): class BasicChannel(ZigbeeChannel): """Channel to interact with the basic cluster.""" + UNKNOWN = 0 BATTERY = 3 + POWER_SOURCES = { - 0: 'Unknown', + UNKNOWN: 'Unknown', 1: 'Mains (single phase)', 2: 'Mains (3 phase)', BATTERY: 'Battery', diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 12bb397fbc3..1ee800d8559 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -146,11 +146,11 @@ class ZHADevice: self._available_signal, False ) - async_dispatcher_send( - self.hass, - "{}_{}".format(self._available_signal, 'entity'), - True - ) + async_dispatcher_send( + self.hass, + "{}_{}".format(self._available_signal, 'entity'), + available + ) self._available = available @property diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index a50bfeae1be..cb5e5bf7774 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -36,6 +36,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = {} BINARY_SENSOR_TYPES = {} +SMARTTHINGS_HUMIDITY_CLUSTER = 64581 EntityReference = collections.namedtuple( 'EntityReference', 'reference_id zha_device cluster_channels device_info') @@ -174,7 +175,8 @@ class ZHAGateway: # available and we already loaded fresh state above zha_device.update_available(True) elif not zha_device.available and zha_device.power_source is not None\ - and zha_device.power_source != BasicChannel.BATTERY: + and zha_device.power_source != BasicChannel.BATTERY\ + and zha_device.power_source != BasicChannel.UNKNOWN: # the device is currently marked unavailable and it isn't a battery # powered device so we should be able to update it now _LOGGER.debug( @@ -380,7 +382,10 @@ async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, """Dispatch a single cluster match to a HA component.""" component = None # sub_component = None for cluster_type, candidate_component in device_classes.items(): - if isinstance(cluster, cluster_type): + if isinstance(cluster_type, int): + if cluster.cluster_id == cluster_type: + component = candidate_component + elif isinstance(cluster, cluster_type): component = candidate_component break @@ -473,6 +478,9 @@ def establish_device_mappings(): SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update({ zcl.clusters.general.OnOff: 'switch', zcl.clusters.measurement.RelativeHumidity: 'sensor', + # this works for now but if we hit conflicts we can break it out to + # a different dict that is keyed by manufacturer + SMARTTHINGS_HUMIDITY_CLUSTER: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', zcl.clusters.measurement.PressureMeasurement: 'sensor', zcl.clusters.measurement.IlluminanceMeasurement: 'sensor', @@ -489,6 +497,7 @@ def establish_device_mappings(): SENSOR_TYPES.update({ zcl.clusters.measurement.RelativeHumidity.cluster_id: HUMIDITY, + SMARTTHINGS_HUMIDITY_CLUSTER: HUMIDITY, zcl.clusters.measurement.TemperatureMeasurement.cluster_id: TEMPERATURE, zcl.clusters.measurement.PressureMeasurement.cluster_id: PRESSURE, From 09692143d097c0e278adcbd6eca214723cfc711e Mon Sep 17 00:00:00 2001 From: OleksandrBerchenko Date: Thu, 21 Feb 2019 15:48:17 +0200 Subject: [PATCH 098/253] Correctly detect devices, which went offline during HA restart (#20933) * Correctly detect devices, which went offline during HA restart * Update __init__.py --- homeassistant/components/device_tracker/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index af33453c9d5..7d8449197de 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -580,6 +580,7 @@ class Device(RestoreEntity): return self._state = state.state self.last_update_home = (state.state == STATE_HOME) + self.last_seen = dt_util.utcnow() for attr, var in ( (ATTR_SOURCE_TYPE, 'source_type'), From e764d9461a19f2a4f0bc66145531c96f65c7d590 Mon Sep 17 00:00:00 2001 From: Gido Date: Thu, 21 Feb 2019 15:51:36 +0100 Subject: [PATCH 099/253] Update rova component with suffix for house number (#21182) * Update rova component with release rova release 0.1.0 Add house_number_suffix to configuration * Set default value for house_number_suffix --- homeassistant/components/sensor/rova.py | 7 +++++-- requirements_all.txt | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/rova.py b/homeassistant/components/sensor/rova.py index 0b7f43f0973..07be331f23f 100644 --- a/homeassistant/components/sensor/rova.py +++ b/homeassistant/components/sensor/rova.py @@ -17,11 +17,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['rova==0.0.2'] +REQUIREMENTS = ['rova==0.1.0'] # Config for rova requests. CONF_ZIP_CODE = 'zip_code' CONF_HOUSE_NUMBER = 'house_number' +CONF_HOUSE_NUMBER_SUFFIX = 'house_number_suffix' UPDATE_DELAY = timedelta(hours=12) SCAN_INTERVAL = timedelta(hours=12) @@ -37,6 +38,7 @@ SENSOR_TYPES = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ZIP_CODE): cv.string, vol.Required(CONF_HOUSE_NUMBER): cv.string, + vol.Optional(CONF_HOUSE_NUMBER_SUFFIX, default=''): cv.string, vol.Optional(CONF_NAME, default='Rova'): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=['bio']): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]) @@ -52,10 +54,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): zip_code = config[CONF_ZIP_CODE] house_number = config[CONF_HOUSE_NUMBER] + house_number_suffix = config[CONF_HOUSE_NUMBER_SUFFIX] platform_name = config[CONF_NAME] # Create new Rova object to retrieve data - api = Rova(zip_code, house_number) + api = Rova(zip_code, house_number, house_number_suffix) try: if not api.is_rova_area(): diff --git a/requirements_all.txt b/requirements_all.txt index 4e4e8b78962..80c3e9a5a6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1505,7 +1505,7 @@ rocketchat-API==0.6.1 roombapy==1.3.1 # homeassistant.components.sensor.rova -rova==0.0.2 +rova==0.1.0 # homeassistant.components.switch.rpi_rf # rpi-rf==0.9.7 From 6b7a5cfcadf56951657360e789a76aa7b9bbffb1 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 21 Feb 2019 17:26:09 +0100 Subject: [PATCH 100/253] Add missing configuration option to plex (#21264) Fixes part of: https://github.com/home-assistant/home-assistant/issues/21254 --- homeassistant/components/media_player/plex.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index c67849edee9..35000fa35c3 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -47,6 +47,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_INCLUDE_NON_CLIENTS, default=False): cv.boolean, vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, vol.Optional(CONF_USE_CUSTOM_ENTITY_IDS, default=False): cv.boolean, + vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean, vol.Optional(CONF_REMOVE_UNAVAILABLE_CLIENTS, default=True): cv.boolean, vol.Optional(CONF_CLIENT_REMOVE_INTERVAL, default=timedelta(seconds=600)): vol.All(cv.time_period, cv.positive_timedelta), From 998b5f6d19db75d1c359cd3226f3520015edfcf4 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 21 Feb 2019 17:26:26 +0100 Subject: [PATCH 101/253] Add missing configation option (#21265) Fixes https://github.com/home-assistant/home-assistant/issues/21254 --- homeassistant/components/sensor/imap_email_content.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/imap_email_content.py b/homeassistant/components/sensor/imap_email_content.py index 225ad08f7d1..714a6c38781 100644 --- a/homeassistant/components/sensor/imap_email_content.py +++ b/homeassistant/components/sensor/imap_email_content.py @@ -35,6 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_SERVER): cv.string, + vol.Required(CONF_SENDERS): [cv.string], vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_FOLDER, default='INBOX'): cv.string, From 4c4317fb377673118ecb334a85e472de047e7038 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Feb 2019 08:52:45 -0800 Subject: [PATCH 102/253] Add SamsungTV Mac validation (#21268) --- homeassistant/components/media_player/samsungtv.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index db6bd317c40..e6715669da7 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -42,6 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_MAC): cv.string, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, }) From 565f513b772d8c063c82c6bbdc0d197750503ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=BDdrale?= Date: Thu, 21 Feb 2019 18:24:37 +0100 Subject: [PATCH 103/253] Added device tracker support for Ubee Router (#19586) * Added Ubee Router Device Tracker. * Updated code to meet requirements. * Code clean-up. * Code clean-up. * Code clean-up. * Minor error message update * Ubee device tracker: Minor code clean-up * Bump pyubee version * Code clean-up --- .coveragerc | 1 + .../components/device_tracker/ubee.py | 92 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 96 insertions(+) create mode 100644 homeassistant/components/device_tracker/ubee.py diff --git a/.coveragerc b/.coveragerc index df2e0df2aed..3f36e698dac 100644 --- a/.coveragerc +++ b/.coveragerc @@ -136,6 +136,7 @@ omit = homeassistant/components/device_tracker/tplink.py homeassistant/components/device_tracker/traccar.py homeassistant/components/device_tracker/trackr.py + homeassistant/components/device_tracker/ubee.py homeassistant/components/device_tracker/ubus.py homeassistant/components/digital_ocean/* homeassistant/components/dominos/* diff --git a/homeassistant/components/device_tracker/ubee.py b/homeassistant/components/device_tracker/ubee.py new file mode 100644 index 00000000000..f4ecc7d4855 --- /dev/null +++ b/homeassistant/components/device_tracker/ubee.py @@ -0,0 +1,92 @@ +""" +Support for Ubee router. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.ubee/ +""" + +import logging +import voluptuous as vol + +from homeassistant.components.device_tracker import ( + DOMAIN, PLATFORM_SCHEMA, DeviceScanner) +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_USERNAME) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pyubee==0.2'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, +}) + + +def get_scanner(hass, config): + """Validate the configuration and return a Ubee scanner.""" + try: + return UbeeDeviceScanner(config[DOMAIN]) + except ConnectionError: + return None + + +class UbeeDeviceScanner(DeviceScanner): + """This class queries a wireless Ubee router.""" + + def __init__(self, config): + """Initialize the Ubee scanner.""" + from pyubee import Ubee + + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + self.last_results = {} + self.mac2name = {} + + self.ubee = Ubee(self.host, self.username, self.password) + _LOGGER.info("Logging in") + results = self.get_connected_devices() + self.success_init = results is not None + + if self.success_init: + self.last_results = results + else: + _LOGGER.error("Login failed") + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + + return self.last_results + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + if device in self.mac2name: + return self.mac2name.get(device) + + return None + + def _update_info(self): + """Retrieve latest information from the Ubee router.""" + if not self.success_init: + return + + _LOGGER.debug("Scanning") + results = self.get_connected_devices() + + if results is None: + _LOGGER.warning("Error scanning devices") + return + + self.last_results = results or [] + + def get_connected_devices(self): + """List connected devices with pyubee.""" + if not self.ubee.session_active(): + self.ubee.login() + + return self.ubee.get_connected_devices() diff --git a/requirements_all.txt b/requirements_all.txt index 80c3e9a5a6a..3345af80f09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1417,6 +1417,9 @@ pytradfri[async]==6.0.1 # homeassistant.components.sensor.trafikverket_weatherstation pytrafikverket==0.1.5.8 +# homeassistant.components.device_tracker.ubee +pyubee==0.2 + # homeassistant.components.device_tracker.unifi pyunifi==2.16 From c637bad1ebff1347ce85436b055e5abea5c4a83d Mon Sep 17 00:00:00 2001 From: Tobias Hoff Date: Thu, 21 Feb 2019 19:35:27 +0100 Subject: [PATCH 104/253] account specific cookies file to enable multiple accounts (#19811) --- homeassistant/components/device_tracker/google_maps.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index c324f3c2757..3de60d6cb38 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -61,8 +61,9 @@ class GoogleMapsScanner: self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY] try: - self.service = Service(self.username, self.password, - hass.config.path(CREDENTIALS_FILE)) + credfile = "{}.{}".format(hass.config.path(CREDENTIALS_FILE), + slugify(self.username)) + self.service = Service(self.username, self.password, credfile) self._update_info() track_time_interval( From 94be43e3e1907cefce22a7a54f1607f29b61fa9f Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 21 Feb 2019 20:29:07 +0100 Subject: [PATCH 105/253] Add support for automatic discovery of TP-Link switches, bulbs and dimmers (#18091) * {switch,light}.tplink: use deviceid as unique id, fetch name from the device during initialization * raise PlatformNotReady when no device is available * Use mac instead of deviceid * remove name option as obsolete * Add support for configuration flow / integration Allows activating automatic discovery of supported devices from the configuration * Fix linting, update requirements_all.txt * start cleaning up tplink component based on feedback * add device info, improve config handling * Allow overriding detected devices via configuration file * Update requirements.txt * Remove debug logging * make hound happy * Avoid I/O during init and simplify the code, remove remains of leds_on * Fix issues based on feedback, use consistent quotation marks for device info * add async_setup_platform emiting a deprecation warning * Avoid blocking the I/O, check for None on features * handle some Martin's comments, schema-validation is still missing * use async_create_task instead of async_add_job, let core validate the schema * simplify configuration handling by storing the configuration data separately from initialized instances * add default values to schema, make hound happy * with defaults set by schema, simplify the checks. add async_unload_entry * Use constant for data structure access * REWORD add a short note about async_unload_entry * handle feedback from Martin, config_data is checked against Noneness * use pop to remove the domain on unload * First steps to add tests for the new tplink component * embed platforms under the component directory * Fix tests by mocking the pyhs100 internals * Fix linting * Test against multiple instances of devices, tidy up * (hopefully) final linting round * Add pyHS100 to test requirements * log always the warnings occured during an update to make them easy to see * revert back the warning behavior (requirement for silver level in IQS) * Unload only when an entry is being loaded and add tests for that Thanks @MartinHjelmare for pointing this out! * Fix linting * Bump the upstream lib, fixes most prominently the HSV setting on bulbs * Test unloading for all platforms, clear the data storage instead of popping it out, making it possible to reconfigure after removal without restarting hass first * Use class variables instead of instance variables for bulb states, required for HS220 * Use new-style format string * Fix indenting, uppercase the mock constant * Run black on test_init, hopefully that will finally fix the weird formatting (pycharm, pylint and hound seems to have different opinions...) --- .../components/tplink/.translations/en.json | 15 ++ homeassistant/components/tplink/__init__.py | 154 +++++++++++++++ .../{light/tplink.py => tplink/light.py} | 96 ++++++---- homeassistant/components/tplink/strings.json | 15 ++ .../{switch/tplink.py => tplink/switch.py} | 88 +++++---- homeassistant/config_entries.py | 3 +- requirements_all.txt | 3 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/tplink/__init__.py | 1 + tests/components/tplink/test_init.py | 181 ++++++++++++++++++ 11 files changed, 484 insertions(+), 76 deletions(-) create mode 100644 homeassistant/components/tplink/.translations/en.json create mode 100644 homeassistant/components/tplink/__init__.py rename homeassistant/components/{light/tplink.py => tplink/light.py} (72%) create mode 100644 homeassistant/components/tplink/strings.json rename homeassistant/components/{switch/tplink.py => tplink/switch.py} (59%) create mode 100644 tests/components/tplink/__init__.py create mode 100644 tests/components/tplink/test_init.py diff --git a/homeassistant/components/tplink/.translations/en.json b/homeassistant/components/tplink/.translations/en.json new file mode 100644 index 00000000000..e353c1363ab --- /dev/null +++ b/homeassistant/components/tplink/.translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "title": "TP-Link Smart Home", + "step": { + "confirm": { + "title": "TP-Link Smart Home", + "description": "Do you want to setup TP-Link smart devices?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration is necessary.", + "no_devices_found": "No TP-Link devices found on the network." + } + } +} diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py new file mode 100644 index 00000000000..bc285150890 --- /dev/null +++ b/homeassistant/components/tplink/__init__.py @@ -0,0 +1,154 @@ +"""Component to embed TP-Link smart home devices.""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_HOST +from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'tplink' + +TPLINK_HOST_SCHEMA = vol.Schema({ + vol.Required(CONF_HOST): cv.string +}) + +CONF_LIGHT = 'light' +CONF_SWITCH = 'switch' +CONF_DISCOVERY = 'discovery' + +ATTR_CONFIG = 'config' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional('light', default=[]): vol.All(cv.ensure_list, + [TPLINK_HOST_SCHEMA]), + vol.Optional('switch', default=[]): vol.All(cv.ensure_list, + [TPLINK_HOST_SCHEMA]), + vol.Optional('discovery', default=True): cv.boolean, + }), +}, extra=vol.ALLOW_EXTRA) + +REQUIREMENTS = ['pyHS100==0.3.4'] + + +async def _async_has_devices(hass): + """Return if there are devices that can be discovered.""" + from pyHS100 import Discover + + def discover(): + devs = Discover.discover() + return devs + return await hass.async_add_executor_job(discover) + + +async def async_setup(hass, config): + """Set up the TP-Link component.""" + conf = config.get(DOMAIN) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][ATTR_CONFIG] = conf + + if conf is not None: + hass.async_create_task(hass.config_entries.flow.async_init( + DOMAIN, context={'source': config_entries.SOURCE_IMPORT})) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up TPLink from a config entry.""" + from pyHS100 import SmartBulb, SmartPlug, SmartDeviceException + + devices = {} + + config_data = hass.data[DOMAIN].get(ATTR_CONFIG) + + # These will contain the initialized devices + lights = hass.data[DOMAIN][CONF_LIGHT] = [] + switches = hass.data[DOMAIN][CONF_SWITCH] = [] + + # If discovery is defined and not disabled, discover devices + # If initialized from configure integrations, there's no config + # so we default here to True + if config_data is None or config_data[CONF_DISCOVERY]: + devs = await _async_has_devices(hass) + _LOGGER.info("Discovered %s TP-Link smart home device(s)", len(devs)) + devices.update(devs) + + def _device_for_type(host, type_): + dev = None + if type_ == CONF_LIGHT: + dev = SmartBulb(host) + elif type_ == CONF_SWITCH: + dev = SmartPlug(host) + + return dev + + # When arriving from configure integrations, we have no config data. + if config_data is not None: + for type_ in [CONF_LIGHT, CONF_SWITCH]: + for entry in config_data[type_]: + try: + host = entry['host'] + dev = _device_for_type(host, type_) + devices[host] = dev + _LOGGER.debug("Succesfully added %s %s: %s", + type_, host, dev) + except SmartDeviceException as ex: + _LOGGER.error("Unable to initialize %s %s: %s", + type_, host, ex) + + # This is necessary to avoid I/O blocking on is_dimmable + def _fill_device_lists(): + for dev in devices.values(): + if isinstance(dev, SmartPlug): + if dev.is_dimmable: # Dimmers act as lights + lights.append(dev) + else: + switches.append(dev) + elif isinstance(dev, SmartBulb): + lights.append(dev) + else: + _LOGGER.error("Unknown smart device type: %s", type(dev)) + + # Avoid blocking on is_dimmable + await hass.async_add_executor_job(_fill_device_lists) + + forward_setup = hass.config_entries.async_forward_entry_setup + if lights: + _LOGGER.debug("Got %s lights: %s", len(lights), lights) + hass.async_create_task(forward_setup(config_entry, 'light')) + if switches: + _LOGGER.debug("Got %s switches: %s", len(switches), switches) + hass.async_create_task(forward_setup(config_entry, 'switch')) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + forward_unload = hass.config_entries.async_forward_entry_unload + remove_lights = remove_switches = False + if hass.data[DOMAIN][CONF_LIGHT]: + remove_lights = await forward_unload(entry, 'light') + if hass.data[DOMAIN][CONF_SWITCH]: + remove_switches = await forward_unload(entry, 'switch') + + if remove_lights or remove_switches: + hass.data[DOMAIN].clear() + return True + + # We were not able to unload the platforms, either because there + # were none or one of the forward_unloads failed. + return False + + +config_entry_flow.register_discovery_flow(DOMAIN, + 'TP-Link Smart Home', + _async_has_devices, + config_entries.CONN_CLASS_LOCAL_POLL) diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/tplink/light.py similarity index 72% rename from homeassistant/components/light/tplink.py rename to homeassistant/components/tplink/light.py index bd1621a0b35..5d22b1ae60f 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/tplink/light.py @@ -7,19 +7,20 @@ https://home-assistant.io/components/light.tplink/ import logging import time -import voluptuous as vol - -from homeassistant.const import (CONF_HOST, CONF_NAME) from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA) -import homeassistant.helpers.config_validation as cv + SUPPORT_COLOR_TEMP, SUPPORT_COLOR) from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired) +import homeassistant.helpers.device_registry as dr +from homeassistant.components.tplink import (DOMAIN as TPLINK_DOMAIN, + CONF_LIGHT) -REQUIREMENTS = ['pyHS100==0.3.4'] +DEPENDENCIES = ['tplink'] + +PARALLEL_UPDATES = 0 _LOGGER = logging.getLogger(__name__) @@ -27,20 +28,25 @@ ATTR_CURRENT_POWER_W = 'current_power_w' ATTR_DAILY_ENERGY_KWH = 'daily_energy_kwh' ATTR_MONTHLY_ENERGY_KWH = 'monthly_energy_kwh' -DEFAULT_NAME = 'TP-Link Light' -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string -}) +def async_setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the platform. + + Deprecated. + """ + _LOGGER.warning('Loading as a platform is deprecated, ' + 'convert to use the tplink component.') -def setup_platform(hass, config, add_entities, discovery_info=None): - """Initialise pyLB100 SmartBulb.""" - from pyHS100 import SmartBulb - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - add_entities([TPLinkSmartBulb(SmartBulb(host), name)], True) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up discovered switches.""" + devs = [] + for dev in hass.data[TPLINK_DOMAIN][CONF_LIGHT]: + devs.append(TPLinkSmartBulb(dev)) + + async_add_entities(devs, True) + + return True def brightness_to_percentage(byt): @@ -56,25 +62,42 @@ def brightness_from_percentage(percent): class TPLinkSmartBulb(Light): """Representation of a TPLink Smart Bulb.""" - # F821: https://github.com/PyCQA/pyflakes/issues/373 - def __init__(self, smartbulb: 'SmartBulb', name) -> None: # noqa: F821 + def __init__(self, smartbulb) -> None: """Initialize the bulb.""" self.smartbulb = smartbulb - self._name = name + self._sysinfo = None self._state = None - self._available = True + self._available = False self._color_temp = None self._brightness = None self._hs = None - self._supported_features = 0 + self._supported_features = None self._min_mireds = None self._max_mireds = None self._emeter_params = {} + @property + def unique_id(self): + """Return a unique ID.""" + return self._sysinfo["mac"] + @property def name(self): - """Return the name of the Smart Bulb, if any.""" - return self._name + """Return the name of the Smart Bulb.""" + return self._sysinfo["alias"] + + @property + def device_info(self): + """Return information about the device.""" + return { + "name": self.name, + "model": self._sysinfo["model"], + "manufacturer": 'TP-Link', + "connections": { + (dr.CONNECTION_NETWORK_MAC, self._sysinfo["mac"]) + }, + "sw_version": self._sysinfo["sw_ver"], + } @property def available(self) -> bool: @@ -88,7 +111,8 @@ class TPLinkSmartBulb(Light): def turn_on(self, **kwargs): """Turn the light on.""" - self.smartbulb.state = self.smartbulb.BULB_STATE_ON + from pyHS100 import SmartBulb + self.smartbulb.state = SmartBulb.BULB_STATE_ON if ATTR_COLOR_TEMP in kwargs: self.smartbulb.color_temp = \ @@ -105,7 +129,8 @@ class TPLinkSmartBulb(Light): def turn_off(self, **kwargs): """Turn the light off.""" - self.smartbulb.state = self.smartbulb.BULB_STATE_OFF + from pyHS100 import SmartBulb + self.smartbulb.state = SmartBulb.BULB_STATE_OFF @property def min_mireds(self): @@ -139,17 +164,13 @@ class TPLinkSmartBulb(Light): def update(self): """Update the TP-Link Bulb's state.""" - from pyHS100 import SmartDeviceException + from pyHS100 import SmartDeviceException, SmartBulb try: - if self._supported_features == 0: + if self._supported_features is None: self.get_features() self._state = ( - self.smartbulb.state == self.smartbulb.BULB_STATE_ON) - - # Pull the name from the device if a name was not specified - if self._name == DEFAULT_NAME: - self._name = self.smartbulb.alias + self.smartbulb.state == SmartBulb.BULB_STATE_ON) if self._supported_features & SUPPORT_BRIGHTNESS: self._brightness = brightness_from_percentage( @@ -185,9 +206,9 @@ class TPLinkSmartBulb(Light): except (SmartDeviceException, OSError) as ex: if self._available: - _LOGGER.warning( - "Could not read state for %s: %s", self._name, ex) - self._available = False + _LOGGER.warning("Could not read state for %s: %s", + self.smartbulb.host, ex) + self._available = False @property def supported_features(self): @@ -196,6 +217,9 @@ class TPLinkSmartBulb(Light): def get_features(self): """Determine all supported features in one go.""" + self._sysinfo = self.smartbulb.sys_info + self._supported_features = 0 + if self.smartbulb.is_dimmable: self._supported_features += SUPPORT_BRIGHTNESS if self.smartbulb.is_variable_color_temp: diff --git a/homeassistant/components/tplink/strings.json b/homeassistant/components/tplink/strings.json new file mode 100644 index 00000000000..e353c1363ab --- /dev/null +++ b/homeassistant/components/tplink/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "title": "TP-Link Smart Home", + "step": { + "confirm": { + "title": "TP-Link Smart Home", + "description": "Do you want to setup TP-Link smart devices?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration is necessary.", + "no_devices_found": "No TP-Link devices found on the network." + } + } +} diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/tplink/switch.py similarity index 59% rename from homeassistant/components/switch/tplink.py rename to homeassistant/components/tplink/switch.py index 67c8094a1f2..efff0eb4f51 100644 --- a/homeassistant/components/switch/tplink.py +++ b/homeassistant/components/tplink/switch.py @@ -7,58 +7,77 @@ https://home-assistant.io/components/switch.tplink/ import logging import time -import voluptuous as vol - from homeassistant.components.switch import ( - SwitchDevice, PLATFORM_SCHEMA, ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH) -from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_VOLTAGE) -import homeassistant.helpers.config_validation as cv + SwitchDevice, ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH) +from homeassistant.components.tplink import (DOMAIN as TPLINK_DOMAIN, + CONF_SWITCH) +from homeassistant.const import ATTR_VOLTAGE +import homeassistant.helpers.device_registry as dr -REQUIREMENTS = ['pyHS100==0.3.4'] +DEPENDENCIES = ['tplink'] + +PARALLEL_UPDATES = 0 _LOGGER = logging.getLogger(__name__) ATTR_TOTAL_ENERGY_KWH = 'total_energy_kwh' ATTR_CURRENT_A = 'current_a' -CONF_LEDS = 'enable_leds' -DEFAULT_NAME = 'TP-Link Switch' +def async_setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the platform. -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_LEDS): cv.boolean, -}) + Deprecated. + """ + _LOGGER.warning('Loading as a platform is deprecated, ' + 'convert to use the tplink component.') -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the TPLink switch platform.""" - from pyHS100 import SmartPlug - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - leds_on = config.get(CONF_LEDS) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up discovered switches.""" + devs = [] + for dev in hass.data[TPLINK_DOMAIN][CONF_SWITCH]: + devs.append(SmartPlugSwitch(dev)) - add_entities([SmartPlugSwitch(SmartPlug(host), name, leds_on)], True) + async_add_entities(devs, True) + + return True class SmartPlugSwitch(SwitchDevice): """Representation of a TPLink Smart Plug switch.""" - def __init__(self, smartplug, name, leds_on): + def __init__(self, smartplug): """Initialize the switch.""" self.smartplug = smartplug - self._name = name - self._leds_on = leds_on + self._sysinfo = None self._state = None - self._available = True + self._available = False # Set up emeter cache self._emeter_params = {} + @property + def unique_id(self): + """Return a unique ID.""" + return self._sysinfo["mac"] + @property def name(self): - """Return the name of the Smart Plug, if any.""" - return self._name + """Return the name of the Smart Plug.""" + return self._sysinfo["alias"] + + @property + def device_info(self): + """Return information about the device.""" + return { + "name": self.name, + "model": self._sysinfo["model"], + "manufacturer": 'TP-Link', + "connections": { + (dr.CONNECTION_NETWORK_MAC, self._sysinfo["mac"]) + }, + "sw_version": self._sysinfo["sw_ver"], + } @property def available(self) -> bool: @@ -87,17 +106,12 @@ class SmartPlugSwitch(SwitchDevice): """Update the TP-Link switch's state.""" from pyHS100 import SmartDeviceException try: + if not self._sysinfo: + self._sysinfo = self.smartplug.sys_info + self._state = self.smartplug.state == \ self.smartplug.SWITCH_STATE_ON - if self._leds_on is not None: - self.smartplug.led = self._leds_on - self._leds_on = None - - # Pull the name from the device if a name was not specified - if self._name == DEFAULT_NAME: - self._name = self.smartplug.alias - if self.smartplug.has_emeter: emeter_readings = self.smartplug.get_emeter_realtime() @@ -123,6 +137,6 @@ class SmartPlugSwitch(SwitchDevice): except (SmartDeviceException, OSError) as ex: if self._available: - _LOGGER.warning( - "Could not read state for %s: %s", self.name, ex) - self._available = False + _LOGGER.warning("Could not read state for %s: %s", + self.smartplug.host, ex) + self._available = False diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7c2a3155557..7ff93051c9d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -172,13 +172,14 @@ FLOWS = [ 'smhi', 'sonos', 'tellduslive', + 'tplink', 'tradfri', 'twilio', 'unifi', 'upnp', 'zha', 'zone', - 'zwave' + 'zwave', ] diff --git a/requirements_all.txt b/requirements_all.txt index 3345af80f09..2f71a3f14cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -892,8 +892,7 @@ py17track==2.1.1 # homeassistant.components.hdmi_cec pyCEC==0.4.13 -# homeassistant.components.light.tplink -# homeassistant.components.switch.tplink +# homeassistant.components.tplink pyHS100==0.3.4 # homeassistant.components.air_quality.norway_air diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2f16502ea8..f35d582bcab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -180,6 +180,9 @@ pushbullet.py==0.11.0 # homeassistant.components.canary py-canary==0.5.0 +# homeassistant.components.tplink +pyHS100==0.3.4 + # homeassistant.components.media_player.blackbird pyblackbird==0.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 85d9c95aec7..24d081349a0 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -105,6 +105,7 @@ TEST_REQUIREMENTS = ( 'pyunifi', 'pyupnp-async', 'pywebpush', + 'pyHS100', 'regenmaschine', 'restrictedpython', 'rflink', diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py new file mode 100644 index 00000000000..865c6c1d97a --- /dev/null +++ b/tests/components/tplink/__init__.py @@ -0,0 +1 @@ +"""Tests for the TP-Link component.""" diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py new file mode 100644 index 00000000000..1b234428c94 --- /dev/null +++ b/tests/components/tplink/test_init.py @@ -0,0 +1,181 @@ +"""Tests for the TP-Link component.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import tplink +from homeassistant.setup import async_setup_component +from pyHS100 import SmartPlug, SmartBulb +from tests.common import MockDependency, MockConfigEntry, mock_coro + +MOCK_PYHS100 = MockDependency("pyHS100") + + +async def test_creating_entry_tries_discover(hass): + """Test setting up does discovery.""" + with MOCK_PYHS100, patch( + "homeassistant.components.tplink.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup, patch( + "pyHS100.Discover.discover", return_value={"host": 1234} + ): + result = await hass.config_entries.flow.async_init( + tplink.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 1 + + +async def test_configuring_tplink_causes_discovery(hass): + """Test that specifying empty config does discovery.""" + with MOCK_PYHS100, patch("pyHS100.Discover.discover") as discover: + discover.return_value = {"host": 1234} + await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}}) + await hass.async_block_till_done() + + assert len(discover.mock_calls) == 1 + + +@pytest.mark.parametrize( + "name,cls,platform", + [ + ("pyHS100.SmartPlug", SmartPlug, "switch"), + ("pyHS100.SmartBulb", SmartBulb, "light"), + ], +) +@pytest.mark.parametrize("count", [1, 2, 3]) +async def test_configuring_device_types(hass, name, cls, platform, count): + """Test that light or switch platform list is filled correctly.""" + with patch("pyHS100.Discover.discover") as discover, patch( + "pyHS100.SmartDevice._query_helper" + ): + discovery_data = { + "123.123.123.{}".format(c): cls("123.123.123.123") + for c in range(count) + } + discover.return_value = discovery_data + await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}}) + await hass.async_block_till_done() + + assert len(discover.mock_calls) == 1 + assert len(hass.data[tplink.DOMAIN][platform]) == count + + +async def test_is_dimmable(hass): + """Test that is_dimmable switches are correctly added as lights.""" + with patch("pyHS100.Discover.discover") as discover, patch( + "homeassistant.components.tplink.light.async_setup_entry", + return_value=mock_coro(True), + ) as setup, patch("pyHS100.SmartDevice._query_helper"), patch( + "pyHS100.SmartPlug.is_dimmable", True + ): + dimmable_switch = SmartPlug("123.123.123.123") + discover.return_value = {"host": dimmable_switch} + + await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}}) + await hass.async_block_till_done() + + assert len(discover.mock_calls) == 1 + assert len(setup.mock_calls) == 1 + assert len(hass.data[tplink.DOMAIN]["light"]) == 1 + assert len(hass.data[tplink.DOMAIN]["switch"]) == 0 + + +async def test_configuring_discovery_disabled(hass): + """Test that discover does not get called when disabled.""" + with MOCK_PYHS100, patch( + "homeassistant.components.tplink.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup, patch( + "pyHS100.Discover.discover", return_value=[] + ) as discover: + await async_setup_component( + hass, + tplink.DOMAIN, + {tplink.DOMAIN: {tplink.CONF_DISCOVERY: False}}, + ) + await hass.async_block_till_done() + + assert len(discover.mock_calls) == 0 + assert len(mock_setup.mock_calls) == 1 + + +async def test_platforms_are_initialized(hass): + """Test that platforms are initialized per configuration array.""" + config = { + "tplink": { + "discovery": False, + "light": [{"host": "123.123.123.123"}], + "switch": [{"host": "321.321.321.321"}], + } + } + + with patch("pyHS100.Discover.discover") as discover, patch( + "pyHS100.SmartDevice._query_helper" + ), patch( + "homeassistant.components.tplink.light.async_setup_entry", + return_value=mock_coro(True), + ) as light_setup, patch( + "homeassistant.components.tplink.switch.async_setup_entry", + return_value=mock_coro(True), + ) as switch_setup, patch( + "pyHS100.SmartPlug.is_dimmable", False + ): + # patching is_dimmable is necessray to avoid misdetection as light. + await async_setup_component(hass, tplink.DOMAIN, config) + await hass.async_block_till_done() + + assert len(discover.mock_calls) == 0 + assert len(light_setup.mock_calls) == 1 + assert len(switch_setup.mock_calls) == 1 + + +async def test_no_config_creates_no_entry(hass): + """Test for when there is no tplink in config.""" + with MOCK_PYHS100, patch( + "homeassistant.components.tplink.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup: + await async_setup_component(hass, tplink.DOMAIN, {}) + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 0 + + +@pytest.mark.parametrize("platform", ["switch", "light"]) +async def test_unload(hass, platform): + """Test that the async_unload_entry works.""" + # As we have currently no configuration, we just to pass the domain here. + entry = MockConfigEntry(domain=tplink.DOMAIN) + entry.add_to_hass(hass) + + with patch("pyHS100.SmartDevice._query_helper"), patch( + "homeassistant.components.tplink.{}" + ".async_setup_entry".format(platform), + return_value=mock_coro(True), + ) as light_setup: + config = { + "tplink": { + platform: [{"host": "123.123.123.123"}], + "discovery": False, + } + } + assert await async_setup_component(hass, tplink.DOMAIN, config) + await hass.async_block_till_done() + + assert len(light_setup.mock_calls) == 1 + assert tplink.DOMAIN in hass.data + + assert await tplink.async_unload_entry(hass, entry) + assert not hass.data[tplink.DOMAIN] From a2877c4ea04e050f42ade458f336a833594e5e98 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 21 Feb 2019 14:39:55 -0500 Subject: [PATCH 106/253] update services.yaml (#21276) --- homeassistant/components/zha/services.yaml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index c328d69a6c3..0d7fe06fe25 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -29,9 +29,12 @@ set_zigbee_cluster_attribute: description: >- Set attribute value for the specified cluster on the specified entity. fields: - entity_id: - description: Entity id - example: "binary_sensor.centralite_3130_00e8fb4e_1" + ieee: + description: IEEE address for the device + example: "00:0d:6f:00:05:7d:2d:34" + endpoint_id: + description: Endpoint id for the cluster + example: 1 cluster_id: description: ZCL cluster to retrieve attributes for example: 6 @@ -52,9 +55,12 @@ issue_zigbee_cluster_command: description: >- Issue command on the specified cluster on the specified entity. fields: - entity_id: - description: Entity id - example: "binary_sensor.centralite_3130_00e8fb4e_1" + ieee: + description: IEEE address for the device + example: "00:0d:6f:00:05:7d:2d:34" + endpoint_id: + description: Endpoint id for the cluster + example: 1 cluster_id: description: ZCL cluster to retrieve attributes for example: 6 From d0e88d9628e0a73eb47010ba6aa79bc7572cfec2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 21 Feb 2019 13:27:34 -0700 Subject: [PATCH 107/253] Fix unhandled exception in Ambient PWS config entry (#21278) --- homeassistant/components/ambient_station/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 4464992e5fa..16b86a0e298 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -263,7 +263,8 @@ async def async_setup_entry(hass, config_entry): Client( config_entry.data[CONF_API_KEY], config_entry.data[CONF_APP_KEY], session), - hass.data[DOMAIN][DATA_CONFIG].get(CONF_MONITORED_CONDITIONS, [])) + hass.data[DOMAIN].get(DATA_CONFIG, {}).get( + CONF_MONITORED_CONDITIONS, [])) hass.loop.create_task(ambient.ws_connect()) hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient except WebsocketError as err: From d9a44f2a78b0eeb362ab1f559d92c72389145dfc Mon Sep 17 00:00:00 2001 From: David McNett Date: Thu, 21 Feb 2019 16:24:29 -0600 Subject: [PATCH 108/253] Version bump: python-anthemav to v1.1.9 (#21273) --- homeassistant/components/media_player/anthemav.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index d48f90d2bd7..f867a10ccd0 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['anthemav==1.1.8'] +REQUIREMENTS = ['anthemav==1.1.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2f71a3f14cc..d2bbaefce38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -154,7 +154,7 @@ amcrest==1.2.3 anel_pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.media_player.anthemav -anthemav==1.1.8 +anthemav==1.1.9 # homeassistant.components.apcupsd apcaccess==0.0.13 From ac502980a214d1702c1b486e8b98ce09dea19209 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Feb 2019 14:57:38 -0800 Subject: [PATCH 109/253] Do not warn for internally loaded components (#21287) --- homeassistant/loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 50f8b4338d8..3dfc996bb35 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -83,7 +83,7 @@ def get_platform(hass, # type: HomeAssistant """ # If the platform has a component, we will limit the platform loading path # to be the same source (custom/built-in). - component = get_component(hass, platform_name) + component = _load_file(hass, platform_name, LOOKUP_PATHS) # Until we have moved all platforms under their component/own folder, it # can be that the component is None. @@ -181,7 +181,7 @@ def _load_file(hass, # type: HomeAssistant cache[comp_or_platform] = module - if index == 0: + if module.__name__.startswith(PACKAGE_CUSTOM_COMPONENTS): _LOGGER.warning( 'You are using a custom component for %s which has not ' 'been tested by Home Assistant. This component might ' From bf4fb36bb1b83ffb53b3b02ec441a807a126c8b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Feb 2019 16:39:17 -0800 Subject: [PATCH 110/253] Fix yeelight config validation (#21295) --- homeassistant/components/light/yeelight.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index d22d94f7bbc..b4b540f729b 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -84,15 +84,16 @@ DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean, vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean, vol.Optional(CONF_MODEL): cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, vol.Optional(CONF_CUSTOM_EFFECTS): [{ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_FLOW_PARAMS): YEELIGHT_FLOW_TRANSITION_SCHEMA }] }) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}, }) - SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_FLASH) From 3fcbc36abee7cfb421f1d700e03a3be6ef356629 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Feb 2019 16:40:25 -0800 Subject: [PATCH 111/253] Update translations --- .../ambient_station/.translations/es-419.json | 19 ++++++ .../ambient_station/.translations/hu.json | 19 ++++++ .../ambient_station/.translations/it.json | 18 ++++++ .../ambient_station/.translations/pt.json | 14 ++++- .../ambient_station/.translations/sl.json | 19 ++++++ .../components/auth/.translations/es-419.json | 19 ++++++ .../components/auth/.translations/hu.json | 3 +- .../components/auth/.translations/it.json | 3 +- .../daikin/.translations/es-419.json | 17 ++++++ .../components/daikin/.translations/hu.json | 12 +++- .../components/daikin/.translations/it.json | 19 ++++++ .../deconz/.translations/es-419.json | 3 +- .../components/deconz/.translations/hu.json | 2 +- .../components/deconz/.translations/it.json | 2 +- .../dialogflow/.translations/es-419.json | 18 ++++++ .../dialogflow/.translations/it.json | 18 ++++++ .../ebusd/.translations/es-419.json | 6 ++ .../components/ebusd/.translations/es.json | 6 ++ .../components/ebusd/.translations/hu.json | 6 ++ .../components/ebusd/.translations/no.json | 6 ++ .../components/ebusd/.translations/pt.json | 6 ++ .../components/ebusd/.translations/sl.json | 6 ++ .../components/ebusd/.translations/uk.json | 6 ++ .../emulated_roku/.translations/es-419.json | 17 ++++++ .../emulated_roku/.translations/hu.json | 18 ++++++ .../emulated_roku/.translations/it.json | 17 ++++++ .../emulated_roku/.translations/pt.json | 16 ++++- .../esphome/.translations/es-419.json | 30 ++++++++++ .../components/esphome/.translations/hu.json | 5 ++ .../components/esphome/.translations/it.json | 30 ++++++++++ .../geofency/.translations/es-419.json | 18 ++++++ .../components/geofency/.translations/hu.json | 18 ++++++ .../components/geofency/.translations/it.json | 18 ++++++ .../components/geofency/.translations/pt.json | 18 ++++++ .../gpslogger/.translations/es-419.json | 18 ++++++ .../gpslogger/.translations/hu.json | 18 ++++++ .../gpslogger/.translations/it.json | 18 ++++++ .../gpslogger/.translations/pt.json | 18 ++++++ .../hangouts/.translations/es-419.json | 6 ++ .../.translations/es-419.json | 3 + .../homematicip_cloud/.translations/hu.json | 12 +++- .../homematicip_cloud/.translations/it.json | 18 ++++-- .../ifttt/.translations/es-419.json | 17 ++++++ .../components/ifttt/.translations/it.json | 4 +- .../components/ios/.translations/es-419.json | 14 +++++ .../components/ios/.translations/it.json | 9 +++ .../components/ipma/.translations/es-419.json | 19 ++++++ .../components/ipma/.translations/es.json | 18 ++++++ .../components/ipma/.translations/hu.json | 19 ++++++ .../components/ipma/.translations/it.json | 19 ++++++ .../components/ipma/.translations/no.json | 17 ++++++ .../components/ipma/.translations/sl.json | 19 ++++++ .../components/ipma/.translations/uk.json | 9 +++ .../components/lifx/.translations/es-419.json | 15 +++++ .../components/lifx/.translations/it.json | 15 +++++ .../locative/.translations/es-419.json | 18 ++++++ .../components/locative/.translations/hu.json | 15 +++++ .../components/locative/.translations/it.json | 18 ++++++ .../components/locative/.translations/pt.json | 18 ++++++ .../luftdaten/.translations/es-419.json | 19 ++++++ .../luftdaten/.translations/hu.json | 9 +++ .../luftdaten/.translations/it.json | 19 ++++++ .../mailgun/.translations/es-419.json | 18 ++++++ .../components/mailgun/.translations/it.json | 18 ++++++ .../components/mqtt/.translations/es-419.json | 24 +++++++- .../components/mqtt/.translations/hu.json | 3 +- .../components/mqtt/.translations/it.json | 13 +++- .../components/nest/.translations/es-419.json | 1 + .../components/nest/.translations/hu.json | 1 + .../components/nest/.translations/it.json | 2 +- .../openuv/.translations/es-419.json | 7 ++- .../components/openuv/.translations/it.json | 9 ++- .../owntracks/.translations/es-419.json | 17 ++++++ .../owntracks/.translations/it.json | 14 +++++ .../point/.translations/es-419.json | 24 ++++++++ .../components/point/.translations/hu.json | 9 +++ .../components/point/.translations/it.json | 21 ++++++- .../components/ps4/.translations/ca.json | 32 ++++++++++ .../components/ps4/.translations/cs.json | 13 ++++ .../components/ps4/.translations/da.json | 32 ++++++++++ .../components/ps4/.translations/en.json | 60 +++++++++---------- .../components/ps4/.translations/es-419.json | 32 ++++++++++ .../components/ps4/.translations/it.json | 32 ++++++++++ .../components/ps4/.translations/ko.json | 32 ++++++++++ .../components/ps4/.translations/lb.json | 32 ++++++++++ .../components/ps4/.translations/pl.json | 32 ++++++++++ .../components/ps4/.translations/pt.json | 18 ++++++ .../components/ps4/.translations/ru.json | 32 ++++++++++ .../components/ps4/.translations/zh-Hant.json | 32 ++++++++++ .../rainmachine/.translations/es-419.json | 19 ++++++ .../rainmachine/.translations/hu.json | 4 +- .../rainmachine/.translations/it.json | 19 ++++++ .../sensor/.translations/moon.it.json | 2 +- .../simplisafe/.translations/es-419.json | 19 ++++++ .../smartthings/.translations/ca.json | 3 +- .../smartthings/.translations/da.json | 3 +- .../smartthings/.translations/es-419.json | 25 ++++++++ .../smartthings/.translations/hu.json | 28 +++++++++ .../smartthings/.translations/it.json | 26 ++++++++ .../smartthings/.translations/ko.json | 7 ++- .../smartthings/.translations/lb.json | 3 +- .../smartthings/.translations/pl.json | 3 +- .../smartthings/.translations/pt.json | 12 +++- .../smartthings/.translations/ru.json | 9 +-- .../smartthings/.translations/sl.json | 27 +++++++++ .../smartthings/.translations/zh-Hant.json | 3 +- .../components/smhi/.translations/es-419.json | 19 ++++++ .../components/smhi/.translations/hu.json | 3 +- .../components/smhi/.translations/it.json | 19 ++++++ .../components/sonos/.translations/it.json | 2 +- .../tellduslive/.translations/es-419.json | 23 +++++++ .../tellduslive/.translations/hu.json | 13 +++- .../tellduslive/.translations/it.json | 26 ++++++++ .../tellduslive/.translations/pt.json | 4 ++ .../tellduslive/.translations/zh-Hant.json | 2 +- .../components/tplink/.translations/en.json | 26 ++++---- .../tradfri/.translations/es-419.json | 22 +++++++ .../components/tradfri/.translations/it.json | 18 ++++++ .../twilio/.translations/es-419.json | 18 ++++++ .../components/twilio/.translations/hu.json | 10 +++- .../components/twilio/.translations/it.json | 18 ++++++ .../unifi/.translations/es-419.json | 26 ++++++++ .../components/unifi/.translations/hu.json | 9 ++- .../components/unifi/.translations/it.json | 26 ++++++++ .../components/upnp/.translations/es-419.json | 30 ++++++++++ .../components/upnp/.translations/hu.json | 7 ++- .../components/upnp/.translations/it.json | 30 ++++++++++ .../components/upnp/.translations/ko.json | 3 - .../components/zha/.translations/es-419.json | 20 +++++++ .../components/zha/.translations/hu.json | 1 + .../components/zha/.translations/it.json | 20 +++++++ .../zwave/.translations/es-419.json | 19 ++++++ .../components/zwave/.translations/hu.json | 6 +- .../components/zwave/.translations/it.json | 9 ++- 134 files changed, 1968 insertions(+), 106 deletions(-) create mode 100644 homeassistant/components/ambient_station/.translations/es-419.json create mode 100644 homeassistant/components/ambient_station/.translations/hu.json create mode 100644 homeassistant/components/ambient_station/.translations/it.json create mode 100644 homeassistant/components/ambient_station/.translations/sl.json create mode 100644 homeassistant/components/daikin/.translations/es-419.json create mode 100644 homeassistant/components/daikin/.translations/it.json create mode 100644 homeassistant/components/dialogflow/.translations/es-419.json create mode 100644 homeassistant/components/dialogflow/.translations/it.json create mode 100644 homeassistant/components/ebusd/.translations/es-419.json create mode 100644 homeassistant/components/ebusd/.translations/es.json create mode 100644 homeassistant/components/ebusd/.translations/hu.json create mode 100644 homeassistant/components/ebusd/.translations/no.json create mode 100644 homeassistant/components/ebusd/.translations/pt.json create mode 100644 homeassistant/components/ebusd/.translations/sl.json create mode 100644 homeassistant/components/ebusd/.translations/uk.json create mode 100644 homeassistant/components/emulated_roku/.translations/es-419.json create mode 100644 homeassistant/components/emulated_roku/.translations/hu.json create mode 100644 homeassistant/components/emulated_roku/.translations/it.json create mode 100644 homeassistant/components/esphome/.translations/es-419.json create mode 100644 homeassistant/components/esphome/.translations/it.json create mode 100644 homeassistant/components/geofency/.translations/es-419.json create mode 100644 homeassistant/components/geofency/.translations/hu.json create mode 100644 homeassistant/components/geofency/.translations/it.json create mode 100644 homeassistant/components/geofency/.translations/pt.json create mode 100644 homeassistant/components/gpslogger/.translations/es-419.json create mode 100644 homeassistant/components/gpslogger/.translations/hu.json create mode 100644 homeassistant/components/gpslogger/.translations/it.json create mode 100644 homeassistant/components/gpslogger/.translations/pt.json create mode 100644 homeassistant/components/ifttt/.translations/es-419.json create mode 100644 homeassistant/components/ios/.translations/es-419.json create mode 100644 homeassistant/components/ipma/.translations/es-419.json create mode 100644 homeassistant/components/ipma/.translations/es.json create mode 100644 homeassistant/components/ipma/.translations/hu.json create mode 100644 homeassistant/components/ipma/.translations/it.json create mode 100644 homeassistant/components/ipma/.translations/no.json create mode 100644 homeassistant/components/ipma/.translations/sl.json create mode 100644 homeassistant/components/ipma/.translations/uk.json create mode 100644 homeassistant/components/lifx/.translations/es-419.json create mode 100644 homeassistant/components/lifx/.translations/it.json create mode 100644 homeassistant/components/locative/.translations/es-419.json create mode 100644 homeassistant/components/locative/.translations/hu.json create mode 100644 homeassistant/components/locative/.translations/it.json create mode 100644 homeassistant/components/locative/.translations/pt.json create mode 100644 homeassistant/components/luftdaten/.translations/es-419.json create mode 100644 homeassistant/components/luftdaten/.translations/it.json create mode 100644 homeassistant/components/mailgun/.translations/es-419.json create mode 100644 homeassistant/components/mailgun/.translations/it.json create mode 100644 homeassistant/components/owntracks/.translations/es-419.json create mode 100644 homeassistant/components/owntracks/.translations/it.json create mode 100644 homeassistant/components/point/.translations/es-419.json create mode 100644 homeassistant/components/ps4/.translations/ca.json create mode 100644 homeassistant/components/ps4/.translations/cs.json create mode 100644 homeassistant/components/ps4/.translations/da.json create mode 100644 homeassistant/components/ps4/.translations/es-419.json create mode 100644 homeassistant/components/ps4/.translations/it.json create mode 100644 homeassistant/components/ps4/.translations/ko.json create mode 100644 homeassistant/components/ps4/.translations/lb.json create mode 100644 homeassistant/components/ps4/.translations/pl.json create mode 100644 homeassistant/components/ps4/.translations/pt.json create mode 100644 homeassistant/components/ps4/.translations/ru.json create mode 100644 homeassistant/components/ps4/.translations/zh-Hant.json create mode 100644 homeassistant/components/rainmachine/.translations/es-419.json create mode 100644 homeassistant/components/rainmachine/.translations/it.json create mode 100644 homeassistant/components/simplisafe/.translations/es-419.json create mode 100644 homeassistant/components/smartthings/.translations/es-419.json create mode 100644 homeassistant/components/smartthings/.translations/hu.json create mode 100644 homeassistant/components/smartthings/.translations/it.json create mode 100644 homeassistant/components/smartthings/.translations/sl.json create mode 100644 homeassistant/components/smhi/.translations/es-419.json create mode 100644 homeassistant/components/smhi/.translations/it.json create mode 100644 homeassistant/components/tellduslive/.translations/es-419.json create mode 100644 homeassistant/components/tellduslive/.translations/it.json create mode 100644 homeassistant/components/tradfri/.translations/es-419.json create mode 100644 homeassistant/components/twilio/.translations/es-419.json create mode 100644 homeassistant/components/twilio/.translations/it.json create mode 100644 homeassistant/components/unifi/.translations/es-419.json create mode 100644 homeassistant/components/unifi/.translations/it.json create mode 100644 homeassistant/components/upnp/.translations/es-419.json create mode 100644 homeassistant/components/upnp/.translations/it.json create mode 100644 homeassistant/components/zha/.translations/es-419.json create mode 100644 homeassistant/components/zha/.translations/it.json create mode 100644 homeassistant/components/zwave/.translations/es-419.json diff --git a/homeassistant/components/ambient_station/.translations/es-419.json b/homeassistant/components/ambient_station/.translations/es-419.json new file mode 100644 index 00000000000..268a6ba001e --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Clave de aplicaci\u00f3n y/o clave de API ya registrada", + "invalid_key": "Clave de API y/o clave de aplicaci\u00f3n no v\u00e1lida", + "no_devices": "No se han encontrado dispositivos en la cuenta." + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "app_key": "Clave de aplicaci\u00f3n" + }, + "title": "Completa tu informaci\u00f3n" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/hu.json b/homeassistant/components/ambient_station/.translations/hu.json new file mode 100644 index 00000000000..222b512c39f --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Alkalmaz\u00e1s kulcsot \u00e9s/vagy az API kulcsot m\u00e1r regisztr\u00e1lt\u00e1k", + "invalid_key": "\u00c9rv\u00e9nytelen API kulcs \u00e9s / vagy alkalmaz\u00e1skulcs", + "no_devices": "Nincs a fi\u00f3kodban tal\u00e1lhat\u00f3 eszk\u00f6z" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "app_key": "Alkalmaz\u00e1skulcs" + }, + "title": "T\u00f6ltsd ki az adataid" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/it.json b/homeassistant/components/ambient_station/.translations/it.json new file mode 100644 index 00000000000..f87c987a79f --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "API Key e/o Application Key gi\u00e0 registrata", + "invalid_key": "API Key e/o Application Key non valida", + "no_devices": "Nessun dispositivo trovato nell'account" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "app_key": "Application Key" + }, + "title": "Inserisci i tuoi dati" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/pt.json b/homeassistant/components/ambient_station/.translations/pt.json index 01078bbddfe..92746b29f3d 100644 --- a/homeassistant/components/ambient_station/.translations/pt.json +++ b/homeassistant/components/ambient_station/.translations/pt.json @@ -1,11 +1,19 @@ { "config": { + "error": { + "identifier_exists": "Chave de aplica\u00e7\u00e3o e/ou chave de API j\u00e1 registradas.", + "invalid_key": "Chave de API e/ou chave de aplica\u00e7\u00e3o inv\u00e1lidas", + "no_devices": "Nenhum dispositivo encontrado na conta" + }, "step": { "user": { "data": { - "api_key": "Chave de API" - } + "api_key": "Chave de API", + "app_key": "Chave de aplica\u00e7\u00e3o" + }, + "title": "Preencha as suas informa\u00e7\u00f5es" } - } + }, + "title": "Ambient PWS" } } \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/sl.json b/homeassistant/components/ambient_station/.translations/sl.json new file mode 100644 index 00000000000..906a6b404c4 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/sl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Aplikacijski klju\u010d in / ali klju\u010d API je \u017ee registriran", + "invalid_key": "Neveljaven klju\u010d API in / ali klju\u010d aplikacije", + "no_devices": "V ra\u010dunu ni najdene nobene naprave" + }, + "step": { + "user": { + "data": { + "api_key": "API Klju\u010d", + "app_key": "Klju\u010d aplikacije" + }, + "title": "Izpolnite svoje podatke" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/es-419.json b/homeassistant/components/auth/.translations/es-419.json index 6caa9d49993..852965596e0 100644 --- a/homeassistant/components/auth/.translations/es-419.json +++ b/homeassistant/components/auth/.translations/es-419.json @@ -1,8 +1,27 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "No hay servicios de notificaci\u00f3n disponibles." + }, + "error": { + "invalid_code": "C\u00f3digo inv\u00e1lido, por favor int\u00e9ntelo de nuevo." + }, + "step": { + "init": { + "description": "Por favor seleccione uno de los servicios de notificaci\u00f3n:", + "title": "Configure la contrase\u00f1a de un solo uso entregada por el componente de notificaci\u00f3n" + }, + "setup": { + "description": "Se ha enviado una contrase\u00f1a \u00fanica a trav\u00e9s de **notify.{notify_service}**. Por favor ingr\u00e9selo a continuaci\u00f3n:", + "title": "Verificar la configuracion" + } + } + }, "totp": { "step": { "init": { + "description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanee el c\u00f3digo QR con su aplicaci\u00f3n de autenticaci\u00f3n. Si no tiene uno, le recomendamos [Autenticador de Google] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Despu\u00e9s de escanear el c\u00f3digo, ingrese el c\u00f3digo de seis d\u00edgitos de su aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tiene problemas para escanear el c\u00f3digo QR, realice una configuraci\u00f3n manual con el c\u00f3digo ** ` {code} ` **.", "title": "Configurar la autenticaci\u00f3n de dos factores mediante TOTP" } }, diff --git a/homeassistant/components/auth/.translations/hu.json b/homeassistant/components/auth/.translations/hu.json index 0a3a3c58820..a2d132d9073 100644 --- a/homeassistant/components/auth/.translations/hu.json +++ b/homeassistant/components/auth/.translations/hu.json @@ -9,7 +9,8 @@ }, "step": { "init": { - "description": "V\u00e1lassz \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1st:" + "description": "V\u00e1lassz \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1st:", + "title": "\u00c1ll\u00edtsa be az \u00e9rtes\u00edt\u00e9si \u00f6sszetev\u0151 \u00e1ltal megadott egyszeri jelsz\u00f3t" }, "setup": { "title": "Be\u00e1ll\u00edt\u00e1s ellen\u0151rz\u00e9se" diff --git a/homeassistant/components/auth/.translations/it.json b/homeassistant/components/auth/.translations/it.json index 25dad4c1aeb..be06f0209c4 100644 --- a/homeassistant/components/auth/.translations/it.json +++ b/homeassistant/components/auth/.translations/it.json @@ -9,7 +9,8 @@ }, "step": { "init": { - "description": "Selezionare uno dei servizi di notifica:" + "description": "Selezionare uno dei servizi di notifica:", + "title": "Imposta la password one-time fornita dal componente di notifica" }, "setup": { "description": "\u00c8 stata inviata una password monouso tramite **notify.{notify_service}**. Per favore, inseriscila qui sotto:", diff --git a/homeassistant/components/daikin/.translations/es-419.json b/homeassistant/components/daikin/.translations/es-419.json new file mode 100644 index 00000000000..dae3afdba6f --- /dev/null +++ b/homeassistant/components/daikin/.translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "device_timeout": "Tiempo de espera de conexi\u00f3n al dispositivo." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Introduzca la direcci\u00f3n IP de su Daikin AC.", + "title": "Configurar Daikin AC" + } + }, + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/hu.json b/homeassistant/components/daikin/.translations/hu.json index 623fab6828a..cbca935f551 100644 --- a/homeassistant/components/daikin/.translations/hu.json +++ b/homeassistant/components/daikin/.translations/hu.json @@ -1,11 +1,19 @@ { "config": { + "abort": { + "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk", + "device_fail": "Az eszk\u00f6z l\u00e9trehoz\u00e1sakor v\u00e1ratlan hiba l\u00e9pett fel.", + "device_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00e9sz\u00fcl\u00e9k csatlakoz\u00e1sakor." + }, "step": { "user": { "data": { "host": "Kiszolg\u00e1l\u00f3" - } + }, + "description": "Add meg a Daikin l\u00e9gkond\u00edcion\u00e1l\u00f3 IP-c\u00edm\u00e9t.", + "title": "A Daikin l\u00e9gkond\u00edcion\u00e1l\u00f3 konfigur\u00e1l\u00e1sa" } - } + }, + "title": "Daikin L\u00e9gkond\u00edci\u00f3n\u00e1l\u00f3" } } \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/it.json b/homeassistant/components/daikin/.translations/it.json new file mode 100644 index 00000000000..0b8151d23f6 --- /dev/null +++ b/homeassistant/components/daikin/.translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "device_fail": "Errore inatteso durante la creazione del dispositivo.", + "device_timeout": "Tempo scaduto per la connessione al dispositivo." + }, + "step": { + "user": { + "data": { + "host": "Host" + }, + "description": "Inserisci l'indirizzo IP del tuo Daikin AC.", + "title": "Configura Daikin AC" + } + }, + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/es-419.json b/homeassistant/components/deconz/.translations/es-419.json index ab47a5b43c8..7d8f77fe3a2 100644 --- a/homeassistant/components/deconz/.translations/es-419.json +++ b/homeassistant/components/deconz/.translations/es-419.json @@ -23,7 +23,8 @@ "data": { "allow_clip_sensor": "Permitir la importaci\u00f3n de sensores virtuales", "allow_deconz_groups": "Permitir la importaci\u00f3n de grupos deCONZ" - } + }, + "title": "Opciones de configuraci\u00f3n adicionales para deCONZ" } }, "title": "deCONZ Zigbee gateway" diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index fbb5c26ba04..06211f61bf2 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -12,7 +12,7 @@ "init": { "data": { "host": "H\u00e1zigazda (Host)", - "port": "Port (alap\u00e9rtelmezett \u00e9rt\u00e9k: '80')" + "port": "Port" }, "title": "deCONZ \u00e1tj\u00e1r\u00f3 megad\u00e1sa" }, diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 87dcd0610f2..c0a23d47be3 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -28,6 +28,6 @@ "title": "Opzioni di configurazione extra per deCONZ" } }, - "title": "deCONZ" + "title": "Gateway Zigbee deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/es-419.json b/homeassistant/components/dialogflow/.translations/es-419.json new file mode 100644 index 00000000000..41a66b038f5 --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Dialogflow.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar [integraci\u00f3n de webhook de Dialogflow] ( {dialogflow_url} ). \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de contenido: aplicaci\u00f3n / json \n\n Vea [la documentaci\u00f3n] ( {docs_url} ) para m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1 seguro de que desea configurar Dialogflow?", + "title": "Configurar el Webhook de Dialogflow" + } + }, + "title": "Dialogflow" + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/it.json b/homeassistant/components/dialogflow/.translations/it.json new file mode 100644 index 00000000000..cc1a7ac8510 --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Dialogflow.", + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, dovrai configurare [l'integrazione webhook di Dialogflow]({dialogflow_url})\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n - Content Type: application/json \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare Dialogflow?", + "title": "Configura il webhook di Dialogflow" + } + }, + "title": "Dialogflow" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/es-419.json b/homeassistant/components/ebusd/.translations/es-419.json new file mode 100644 index 00000000000..7a6291e3f17 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/es-419.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "D\u00eda", + "night": "Noche" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/es.json b/homeassistant/components/ebusd/.translations/es.json new file mode 100644 index 00000000000..7a6291e3f17 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/es.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "D\u00eda", + "night": "Noche" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/hu.json b/homeassistant/components/ebusd/.translations/hu.json new file mode 100644 index 00000000000..a5ab8f0d194 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/hu.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Nappal", + "night": "\u00c9jszaka" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/no.json b/homeassistant/components/ebusd/.translations/no.json new file mode 100644 index 00000000000..92f4355066d --- /dev/null +++ b/homeassistant/components/ebusd/.translations/no.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Dag", + "night": "Natt" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/pt.json b/homeassistant/components/ebusd/.translations/pt.json new file mode 100644 index 00000000000..9925fdfab9c --- /dev/null +++ b/homeassistant/components/ebusd/.translations/pt.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Dia", + "night": "Noite" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/sl.json b/homeassistant/components/ebusd/.translations/sl.json new file mode 100644 index 00000000000..de2ca81f8a8 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/sl.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Dan", + "night": "No\u010d" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/uk.json b/homeassistant/components/ebusd/.translations/uk.json new file mode 100644 index 00000000000..2e7a22e49a3 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/uk.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "\u0414\u0435\u043d\u044c", + "night": "\u041d\u0456\u0447" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/es-419.json b/homeassistant/components/emulated_roku/.translations/es-419.json new file mode 100644 index 00000000000..51c18c764db --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "name_exists": "El nombre ya existe" + }, + "step": { + "user": { + "data": { + "host_ip": "IP del host", + "name": "Nombre" + }, + "title": "Definir la configuraci\u00f3n del servidor." + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/hu.json b/homeassistant/components/emulated_roku/.translations/hu.json new file mode 100644 index 00000000000..c38e6890d8a --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" + }, + "step": { + "user": { + "data": { + "host_ip": "H\u00e1zigazda IP", + "listen_port": "Port figyel\u00e9se", + "name": "N\u00e9v" + }, + "title": "A kiszolg\u00e1l\u00f3 szerver konfigur\u00e1l\u00e1sa" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/it.json b/homeassistant/components/emulated_roku/.translations/it.json new file mode 100644 index 00000000000..cba89add799 --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "name_exists": "Il nome \u00e8 gi\u00e0 esistente" + }, + "step": { + "user": { + "data": { + "host_ip": "Indirizzo IP dell'host", + "name": "Nome" + }, + "title": "Definisci la configurazione del server" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/pt.json b/homeassistant/components/emulated_roku/.translations/pt.json index 286cd58dd89..138e077d4a4 100644 --- a/homeassistant/components/emulated_roku/.translations/pt.json +++ b/homeassistant/components/emulated_roku/.translations/pt.json @@ -1,11 +1,21 @@ { "config": { + "abort": { + "name_exists": "Nome j\u00e1 existe" + }, "step": { "user": { "data": { - "name": "Nome" - } + "advertise_ip": "Anuncie o IP", + "advertise_port": "Anuncie porto", + "host_ip": "IP do host", + "listen_port": "Porta \u00e0 escuta", + "name": "Nome", + "upnp_bind_multicast": "Liga\u00e7\u00e3o multicast (Verdadeiro/Falso)" + }, + "title": "Definir configura\u00e7\u00e3o do servidor" } - } + }, + "title": "EmulatedRoku" } } \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/es-419.json b/homeassistant/components/esphome/.translations/es-419.json new file mode 100644 index 00000000000..3557095d094 --- /dev/null +++ b/homeassistant/components/esphome/.translations/es-419.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "ESP ya est\u00e1 configurado" + }, + "error": { + "connection_error": "No se puede conectar a ESP. Aseg\u00farese de que su archivo YAML contenga una l\u00ednea 'api:'.", + "invalid_password": "\u00a1Contrase\u00f1a invalida!", + "resolve_error": "No se puede resolver la direcci\u00f3n de la ESP. Si este error persiste, configure una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "Por favor ingrese la contrase\u00f1a que estableci\u00f3 en su configuraci\u00f3n.", + "title": "Escriba la contrase\u00f1a" + }, + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "description": "Por favor Ingrese la configuraci\u00f3n de conexi\u00f3n de su nodo [ESPHome] (https://esphomelib.com/).", + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/hu.json b/homeassistant/components/esphome/.translations/hu.json index 7fe5da59de6..1e72bd8030c 100644 --- a/homeassistant/components/esphome/.translations/hu.json +++ b/homeassistant/components/esphome/.translations/hu.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Az ESP-t m\u00e1r konfigur\u00e1ltad." + }, "error": { + "connection_error": "Nem tud csatlakozni az ESP-hez. K\u00e9rlek gy\u0151z\u0151dj meg r\u00f3la, hogy a YAML f\u00e1jl tartalmaz egy \"api:\" sort.", "invalid_password": "\u00c9rv\u00e9nytelen jelsz\u00f3!" }, "step": { @@ -8,6 +12,7 @@ "data": { "password": "Jelsz\u00f3" }, + "description": "K\u00e9rj\u00fck, add meg a konfigur\u00e1ci\u00f3ban be\u00e1ll\u00edtott jelsz\u00f3t.", "title": "Adja meg a jelsz\u00f3t" }, "user": { diff --git a/homeassistant/components/esphome/.translations/it.json b/homeassistant/components/esphome/.translations/it.json new file mode 100644 index 00000000000..d3c51f0497f --- /dev/null +++ b/homeassistant/components/esphome/.translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "ESP \u00e8 gi\u00e0 configurato" + }, + "error": { + "connection_error": "Impossibile connettersi ad ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".", + "invalid_password": "Password non valida!", + "resolve_error": "Impossibile risolvere l'indirizzo dell'ESP. Se questo errore persiste, impostare un indirizzo IP statico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "Password" + }, + "description": "Inserisci la password che hai impostato nella tua configurazione.", + "title": "Inserisci la password" + }, + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "description": "Inserisci le impostazioni di connessione del tuo nodo [ESPHome] (https://esphomelib.com/).", + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/es-419.json b/homeassistant/components/geofency/.translations/es-419.json new file mode 100644 index 00000000000..637a430a1f8 --- /dev/null +++ b/homeassistant/components/geofency/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Geofency.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar la funci\u00f3n de webhook en Geofency. \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n\n Vea [la documentaci\u00f3n] ( {docs_url} ) para m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1s seguro de que quieres montar el Webhook de Geofency?", + "title": "Configurar el Webhook de Geofency" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/hu.json b/homeassistant/components/geofency/.translations/hu.json new file mode 100644 index 00000000000..85f71d74434 --- /dev/null +++ b/homeassistant/components/geofency/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Az Home Assistantnek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l, hogy megkapja a Geofency \u00fczeneteit.", + "one_instance_allowed": "Csak egy p\u00e9ld\u00e1ny sz\u00fcks\u00e9ges." + }, + "create_entry": { + "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a Geofencyben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3] ( {docs_url} ) linken tal\u00e1lhat\u00f3k." + }, + "step": { + "user": { + "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Geofency Webhookot?", + "title": "A Geofency Webhook be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/it.json b/homeassistant/components/geofency/.translations/it.json new file mode 100644 index 00000000000..1adad3825a3 --- /dev/null +++ b/homeassistant/components/geofency/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Geofency.", + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in Geofency.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare il webhook di Geofency?", + "title": "Configura il webhook di Geofency" + } + }, + "title": "Webhook di Geofency" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/pt.json b/homeassistant/components/geofency/.translations/pt.json new file mode 100644 index 00000000000..bc68c3ec822 --- /dev/null +++ b/homeassistant/components/geofency/.translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "A sua inst\u00e2ncia Home Assistent precisa de ser acess\u00edvel a partir da internet para receber mensagens IFTTT.", + "one_instance_allowed": "Apenas uma inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar um webhook no Geofency. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Veja [the documentation]({docs_url}) para obter mais detalhes." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o Geofency Webhook?", + "title": "Configurar o Geofency Webhook" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/es-419.json b/homeassistant/components/gpslogger/.translations/es-419.json new file mode 100644 index 00000000000..960198eb04e --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de GPSLogger.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar la funci\u00f3n de webhook en GPSLogger. \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n\n Vea [la documentaci\u00f3n] ( {docs_url} ) para m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1 seguro de que desea configurar el Webhook de GPSLogger?", + "title": "Configurar el Webhook de GPSLogger" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/hu.json b/homeassistant/components/gpslogger/.translations/hu.json new file mode 100644 index 00000000000..2d1dcad2174 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Az Home Assistantnek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l, hogy megkapja a GPSLogger \u00fczeneteit.", + "one_instance_allowed": "Csak egy p\u00e9ld\u00e1ny sz\u00fcks\u00e9ges." + }, + "create_entry": { + "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtanod a webhook funkci\u00f3t a GPSLoggerben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1ld: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3] ( {docs_url} ) linken tal\u00e1lhat\u00f3k." + }, + "step": { + "user": { + "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a GPSLogger Webhookot?", + "title": "GPSLogger Webhook be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/it.json b/homeassistant/components/gpslogger/.translations/it.json new file mode 100644 index 00000000000..aab8edbe44a --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da GPSLogger.", + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook in GPSLogger.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare il webhook di GPSLogger?", + "title": "Configura il webhook di GPSLogger" + } + }, + "title": "Webhook di GPSLogger" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/pt.json b/homeassistant/components/gpslogger/.translations/pt.json new file mode 100644 index 00000000000..4dcfda52753 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "A sua inst\u00e2ncia Home Assistent precisa de ser acess\u00edvel a partir da internet para receber mensagens GPSlogger.", + "one_instance_allowed": "Apenas uma inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar um webhook no GPslogger. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Veja [the documentation]({docs_url}) para obter mais detalhes." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o GPSLogger Webhook?", + "title": "Configurar o Geofency Webhook" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/es-419.json b/homeassistant/components/hangouts/.translations/es-419.json index a3699db08ae..ab78213b53a 100644 --- a/homeassistant/components/hangouts/.translations/es-419.json +++ b/homeassistant/components/hangouts/.translations/es-419.json @@ -4,7 +4,13 @@ "already_configured": "Google Hangouts ya est\u00e1 configurado", "unknown": "Se produjo un error desconocido." }, + "error": { + "invalid_login": "Inicio de sesi\u00f3n no v\u00e1lido, por favor, int\u00e9ntalo de nuevo." + }, "step": { + "2fa": { + "title": "Autenticaci\u00f3n de 2 factores" + }, "user": { "data": { "email": "Direcci\u00f3n de correo electr\u00f3nico", diff --git a/homeassistant/components/homematicip_cloud/.translations/es-419.json b/homeassistant/components/homematicip_cloud/.translations/es-419.json index 8675d6e12b1..61d7defcb66 100644 --- a/homeassistant/components/homematicip_cloud/.translations/es-419.json +++ b/homeassistant/components/homematicip_cloud/.translations/es-419.json @@ -17,6 +17,9 @@ "name": "Nombre (opcional, usado como prefijo de nombre para todos los dispositivos)", "pin": "C\u00f3digo PIN (opcional)" } + }, + "link": { + "description": "Presione el bot\u00f3n azul en el punto de acceso y el bot\u00f3n enviar para registrar HomematicIP con Home Assistant. \n\n ! [Ubicaci\u00f3n del bot\u00f3n en el puente] (/ static / images / config_flows / config_homematicip_cloud.png)" } }, "title": "HomematicIP Cloud" diff --git a/homeassistant/components/homematicip_cloud/.translations/hu.json b/homeassistant/components/homematicip_cloud/.translations/hu.json index cfb4f5e87fd..61ff5ac5fe2 100644 --- a/homeassistant/components/homematicip_cloud/.translations/hu.json +++ b/homeassistant/components/homematicip_cloud/.translations/hu.json @@ -1,19 +1,27 @@ { "config": { "abort": { + "already_configured": "A hozz\u00e1f\u00e9r\u00e9si pontot m\u00e1r konfigur\u00e1ltuk", "connection_aborted": "Nem siker\u00fclt csatlakozni a HMIP szerverhez", "unknown": "Unknown error occurred." }, "error": { "invalid_pin": "\u00c9rv\u00e9nytelen PIN, pr\u00f3b\u00e1lkozz \u00fajra.", "press_the_button": "Nyomd meg a k\u00e9k gombot.", - "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, pr\u00f3b\u00e1ld \u00fajra." + "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, pr\u00f3b\u00e1ld \u00fajra.", + "timeout_button": "K\u00e9k gomb megnyom\u00e1s\u00e1nak id\u0151t\u00fall\u00e9p\u00e9se, pr\u00f3b\u00e1lkozz \u00fajra." }, "step": { "init": { "data": { + "hapid": "Hozz\u00e1f\u00e9r\u00e9si pont azonos\u00edt\u00f3ja (SGTIN)", + "name": "N\u00e9v (opcion\u00e1lis, minden eszk\u00f6z n\u00e9vel\u0151tagjak\u00e9nt haszn\u00e1latos)", "pin": "Pin k\u00f3d (opcion\u00e1lis)" - } + }, + "title": "V\u00e1lassz HomematicIP hozz\u00e1f\u00e9r\u00e9si pontot" + }, + "link": { + "title": "Link Hozz\u00e1f\u00e9r\u00e9si pont" } }, "title": "HomematicIP Felh\u0151" diff --git a/homeassistant/components/homematicip_cloud/.translations/it.json b/homeassistant/components/homematicip_cloud/.translations/it.json index 9ef1abd500c..6e6d7c8a59f 100644 --- a/homeassistant/components/homematicip_cloud/.translations/it.json +++ b/homeassistant/components/homematicip_cloud/.translations/it.json @@ -2,19 +2,29 @@ "config": { "abort": { "already_configured": "Il punto di accesso \u00e8 gi\u00e0 configurato", - "connection_aborted": "Impossibile connettersi al server HMIP" + "connection_aborted": "Impossibile connettersi al server HMIP", + "unknown": "Si \u00e8 verificato un errore sconosciuto." }, "error": { "invalid_pin": "PIN non valido, riprova.", "press_the_button": "Si prega di premere il pulsante blu.", - "register_failed": "Registrazione fallita, si prega di riprovare." + "register_failed": "Registrazione fallita, si prega di riprovare.", + "timeout_button": "Timeout della pressione del pulsante blu, riprovare." }, "step": { "init": { "data": { + "hapid": "ID del punto di accesso (SGTIN)", + "name": "Nome (facoltativo, utilizzato come prefisso del nome per tutti i dispositivi)", "pin": "Codice Pin (opzionale)" - } + }, + "title": "Scegli punto di accesso HomematicIP" + }, + "link": { + "description": "Premi il pulsante blu sull'access point ed il pulsante di invio per registrare HomematicIP con Home Assistant. \n\n ![Posizione del pulsante sul bridge](/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Collegamento access point" } - } + }, + "title": "HomematicIP Cloud" } } \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/es-419.json b/homeassistant/components/ifttt/.translations/es-419.json new file mode 100644 index 00000000000..46096bbe631 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes IFTTT.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 usar la acci\u00f3n \"Realizar una solicitud web\" del [applet de IFTTT Webhook] ( {applet_url} ). \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de contenido: aplicaci\u00f3n / json \n\n Consulte [la documentaci\u00f3n] ( {docs_url} ) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1s seguro de que quieres configurar IFTTT?" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/it.json b/homeassistant/components/ifttt/.translations/it.json index ac81f073347..e5dc76b7923 100644 --- a/homeassistant/components/ifttt/.translations/it.json +++ b/homeassistant/components/ifttt/.translations/it.json @@ -2,14 +2,14 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant deve essere accessibile da internet per ricevere messaggi IFTTT", - "one_instance_allowed": "E' necessaria una sola istanza." + "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { "default": "Per inviare eventi a Home Assistant, dovrai utilizzare l'azione \"Esegui una richiesta web\" dall'applet [Weblet di IFTTT] ( {applet_url} ). \n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Metodo: POST \n - Tipo di contenuto: application / json \n\n Vedi [la documentazione] ( {docs_url} ) su come configurare le automazioni per gestire i dati in arrivo." }, "step": { "user": { - "description": "Sei sicuro di voler impostare IFTTT?", + "description": "Sei sicuro di voler configurare IFTTT?", "title": "Configura l'applet WebHook IFTTT" } }, diff --git a/homeassistant/components/ios/.translations/es-419.json b/homeassistant/components/ios/.translations/es-419.json new file mode 100644 index 00000000000..38a12e7411a --- /dev/null +++ b/homeassistant/components/ios/.translations/es-419.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "S\u00f3lo es necesaria una \u00fanica configuraci\u00f3n de Home Assistant iOS." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar el componente iOS de Home Assistant?", + "title": "Home Assistant iOS" + } + }, + "title": "Home Assistant iOS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/it.json b/homeassistant/components/ios/.translations/it.json index 3f587b7ee64..c2c5042e295 100644 --- a/homeassistant/components/ios/.translations/it.json +++ b/homeassistant/components/ios/.translations/it.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "single_instance_allowed": "\u00c8 necessaria una sola configurazione di Home Assistant iOS." + }, + "step": { + "confirm": { + "description": "Vuoi configurare il componente Home Assistant iOS?", + "title": "Home Assistant iOS" + } + }, "title": "Home Assistant per iOS" } } \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/es-419.json b/homeassistant/components/ipma/.translations/es-419.json new file mode 100644 index 00000000000..acb8b51a44c --- /dev/null +++ b/homeassistant/components/ipma/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "El nombre ya existe" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Ubicaci\u00f3n" + } + }, + "title": "Servicio meteorol\u00f3gico portugu\u00e9s (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/es.json b/homeassistant/components/ipma/.translations/es.json new file mode 100644 index 00000000000..c364ca286e3 --- /dev/null +++ b/homeassistant/components/ipma/.translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "name_exists": "El nombre ya existe" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "title": "Ubicaci\u00f3n" + } + }, + "title": "Servicio meteorol\u00f3gico portugu\u00e9s (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/hu.json b/homeassistant/components/ipma/.translations/hu.json new file mode 100644 index 00000000000..62ddd85e6ef --- /dev/null +++ b/homeassistant/components/ipma/.translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" + }, + "step": { + "user": { + "data": { + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + }, + "description": "Portug\u00e1l Atmoszf\u00e9ra Int\u00e9zet", + "title": "Hely" + } + }, + "title": "Portug\u00e1l Meteorol\u00f3giai Szolg\u00e1lat (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/it.json b/homeassistant/components/ipma/.translations/it.json new file mode 100644 index 00000000000..d751d8a317f --- /dev/null +++ b/homeassistant/components/ipma/.translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Il nome \u00e8 gi\u00e0 esistente" + }, + "step": { + "user": { + "data": { + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Localit\u00e0" + } + }, + "title": "Servizio meteo portoghese (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/no.json b/homeassistant/components/ipma/.translations/no.json new file mode 100644 index 00000000000..a24ec4904cb --- /dev/null +++ b/homeassistant/components/ipma/.translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "name_exists": "Navnet eksisterer allerede" + }, + "step": { + "user": { + "data": { + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn" + }, + "title": "Plassering" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/sl.json b/homeassistant/components/ipma/.translations/sl.json new file mode 100644 index 00000000000..da6a1dac859 --- /dev/null +++ b/homeassistant/components/ipma/.translations/sl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Ime \u017ee obstaja" + }, + "step": { + "user": { + "data": { + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime" + }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", + "title": "Lokacija" + } + }, + "title": "Portugalska vremenska storitev (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/uk.json b/homeassistant/components/ipma/.translations/uk.json new file mode 100644 index 00000000000..bb294cc5d21 --- /dev/null +++ b/homeassistant/components/ipma/.translations/uk.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/es-419.json b/homeassistant/components/lifx/.translations/es-419.json new file mode 100644 index 00000000000..905ec3ce2bf --- /dev/null +++ b/homeassistant/components/lifx/.translations/es-419.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se han encontrado dispositivos LIFX en la red.", + "single_instance_allowed": "S\u00f3lo es posible una \u00fanica configuraci\u00f3n de LIFX." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/it.json b/homeassistant/components/lifx/.translations/it.json new file mode 100644 index 00000000000..b4f940bc66b --- /dev/null +++ b/homeassistant/components/lifx/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo LIFX trovato in rete.", + "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di LIFX." + }, + "step": { + "confirm": { + "description": "Vuoi configurare LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/es-419.json b/homeassistant/components/locative/.translations/es-419.json new file mode 100644 index 00000000000..8fb63ff18c7 --- /dev/null +++ b/homeassistant/components/locative/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Geofency.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar ubicaciones a Home Assistant, deber\u00e1 configurar la funci\u00f3n de webhook en la aplicaci\u00f3n Locative. \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n\n Vea [la documentaci\u00f3n] ( {docs_url} ) para m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1 seguro de que desea configurar el Webhook Locative?", + "title": "Configurar el Webhook Locative" + } + }, + "title": "Webhook Locative" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/hu.json b/homeassistant/components/locative/.translations/hu.json new file mode 100644 index 00000000000..e90910c29a2 --- /dev/null +++ b/homeassistant/components/locative/.translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Az Home Assistantnek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l, hogy megkapja a Geofency \u00fczeneteit.", + "one_instance_allowed": "Csak egy p\u00e9ld\u00e1ny sz\u00fcks\u00e9ges." + }, + "step": { + "user": { + "description": "Biztosan be szeretn\u00e9d be\u00e1ll\u00edtani a Locative Webhookot?", + "title": "Locative Webhook be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/it.json b/homeassistant/components/locative/.translations/it.json new file mode 100644 index 00000000000..de62d2ac2f7 --- /dev/null +++ b/homeassistant/components/locative/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Geofency.", + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "create_entry": { + "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare il webhook di Locative?", + "title": "Configura il webhook di Locative" + } + }, + "title": "Webhook di Locative" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/pt.json b/homeassistant/components/locative/.translations/pt.json new file mode 100644 index 00000000000..2104ad90607 --- /dev/null +++ b/homeassistant/components/locative/.translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "A sua inst\u00e2ncia Home Assistent precisa de ser acess\u00edvel a partir da internet para receber mensagens Geofency.", + "one_instance_allowed": "Apenas uma inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar um webhook no Locative. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Veja [the documentation]({docs_url}) para obter mais detalhes." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o Locative Webhook?", + "title": "Configurar o Locative Webhook" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/.translations/es-419.json b/homeassistant/components/luftdaten/.translations/es-419.json new file mode 100644 index 00000000000..8e81e9e52a1 --- /dev/null +++ b/homeassistant/components/luftdaten/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "communication_error": "No se puede comunicar con la API de Luftdaten", + "invalid_sensor": "Sensor no disponible o no v\u00e1lido", + "sensor_exists": "Sensor ya registrado" + }, + "step": { + "user": { + "data": { + "show_on_map": "Mostrar en el mapa", + "station_id": "ID del sensor de Luftdaten" + }, + "title": "Definir Luftdaten" + } + }, + "title": "Luftdaten" + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/.translations/hu.json b/homeassistant/components/luftdaten/.translations/hu.json index 48914a94465..b8b2b1fc0d8 100644 --- a/homeassistant/components/luftdaten/.translations/hu.json +++ b/homeassistant/components/luftdaten/.translations/hu.json @@ -1,7 +1,16 @@ { "config": { + "error": { + "communication_error": "Nem lehet kommunik\u00e1lni a Luftdaten API-val", + "invalid_sensor": "Az \u00e9rz\u00e9kel\u0151 nem el\u00e9rhet\u0151 vagy \u00e9rv\u00e9nytelen", + "sensor_exists": "Az \u00e9rz\u00e9kel\u0151 m\u00e1r regisztr\u00e1lt" + }, "step": { "user": { + "data": { + "show_on_map": "Mutasd a t\u00e9rk\u00e9pen", + "station_id": "Luftdaten \u00e9rz\u00e9kel\u0151 ID" + }, "title": "Luftdaten be\u00e1ll\u00edt\u00e1sa" } }, diff --git a/homeassistant/components/luftdaten/.translations/it.json b/homeassistant/components/luftdaten/.translations/it.json new file mode 100644 index 00000000000..27951378295 --- /dev/null +++ b/homeassistant/components/luftdaten/.translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "communication_error": "Impossibile comunicare con l'API Luftdaten", + "invalid_sensor": "Sensore non disponibile o non valido", + "sensor_exists": "Sensore gi\u00e0 registrato" + }, + "step": { + "user": { + "data": { + "show_on_map": "Mostra sulla mappa", + "station_id": "ID del sensore Luftdaten" + }, + "title": "Definisci Luftdaten" + } + }, + "title": "Luftdaten" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/es-419.json b/homeassistant/components/mailgun/.translations/es-419.json new file mode 100644 index 00000000000..fd0c543241b --- /dev/null +++ b/homeassistant/components/mailgun/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Mailgun.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar [Webhooks with Mailgun] ( {mailgun_url} ). \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de contenido: aplicaci\u00f3n / json \n\n Consulte [la documentaci\u00f3n] ( {docs_url} ) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1s seguro de que quieres configurar Mailgun?", + "title": "Configurar el Webhook de Mailgun" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/it.json b/homeassistant/components/mailgun/.translations/it.json new file mode 100644 index 00000000000..4dea652aa3f --- /dev/null +++ b/homeassistant/components/mailgun/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Mailgun.", + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhooks con Mailgun]({mailgun_url})\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n - Content Type: application/json\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare Mailgun?", + "title": "Configura il webhook di Mailgun" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/es-419.json b/homeassistant/components/mqtt/.translations/es-419.json index e9e869ae966..4f54e11a112 100644 --- a/homeassistant/components/mqtt/.translations/es-419.json +++ b/homeassistant/components/mqtt/.translations/es-419.json @@ -1,9 +1,31 @@ { "config": { + "abort": { + "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de MQTT." + }, + "error": { + "cannot_connect": "No se puede conectar con el broker." + }, "step": { + "broker": { + "data": { + "broker": "Broker", + "discovery": "Habilitar descubrimiento", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + }, + "description": "Por favor ingrese la informaci\u00f3n de conexi\u00f3n de su agente MQTT.", + "title": "MQTT" + }, "hassio_confirm": { + "data": { + "discovery": "Habilitar descubrimiento" + }, + "description": "\u00bfDesea configurar el Asistente del Hogar para que se conecte al broker MQTT proporcionado por el complemento hass.io {addon}?", "title": "MQTT Broker a trav\u00e9s del complemento Hass.io" } - } + }, + "title": "MQTT" } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/hu.json b/homeassistant/components/mqtt/.translations/hu.json index f08c601633e..26361b0e363 100644 --- a/homeassistant/components/mqtt/.translations/hu.json +++ b/homeassistant/components/mqtt/.translations/hu.json @@ -22,7 +22,8 @@ "data": { "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se" }, - "description": "Szeretn\u00e9d, hogy a Home Assistant csatlakozzon a hass.io addon {addon} \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez?" + "description": "Szeretn\u00e9d, hogy a Home Assistant csatlakozzon a hass.io addon {addon} \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez?", + "title": "MQTT Broker a Hass.io b\u0151v\u00edtm\u00e9nyen kereszt\u00fcl" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/it.json b/homeassistant/components/mqtt/.translations/it.json index e56860cd675..ed33b182a96 100644 --- a/homeassistant/components/mqtt/.translations/it.json +++ b/homeassistant/components/mqtt/.translations/it.json @@ -10,13 +10,20 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Attiva l'individuazione" - } + "discovery": "Attiva l'individuazione", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "description": "Inserisci le informazioni di connessione del tuo broker MQTT.", + "title": "MQTT" }, "hassio_confirm": { "data": { "discovery": "Attiva l'individuazione" - } + }, + "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dall'add-on di Hass.io {addon}?", + "title": "Broker MQTT tramite l'add-on di Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/nest/.translations/es-419.json b/homeassistant/components/nest/.translations/es-419.json index 0dfb5283d8f..117a4500d58 100644 --- a/homeassistant/components/nest/.translations/es-419.json +++ b/homeassistant/components/nest/.translations/es-419.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_setup": "Solo puedes configurar una sola cuenta Nest.", "no_flows": "Debe configurar Nest antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/nest/)." }, "error": { diff --git a/homeassistant/components/nest/.translations/hu.json b/homeassistant/components/nest/.translations/hu.json index e24c38f8608..dc26862f5ea 100644 --- a/homeassistant/components/nest/.translations/hu.json +++ b/homeassistant/components/nest/.translations/hu.json @@ -23,6 +23,7 @@ "data": { "code": "PIN-k\u00f3d" }, + "description": "A Nest-fi\u00f3k \u00f6sszekapcsol\u00e1s\u00e1hoz [enged\u00e9lyezze fi\u00f3kj\u00e1t] ( {url} ). \n\n Az enged\u00e9lyez\u00e9s ut\u00e1n m\u00e1solja be az al\u00e1bbi PIN k\u00f3dot.", "title": "Nest fi\u00f3k \u00f6sszekapcsol\u00e1sa" } }, diff --git a/homeassistant/components/nest/.translations/it.json b/homeassistant/components/nest/.translations/it.json index e4a19ebd521..b55c6d00683 100644 --- a/homeassistant/components/nest/.translations/it.json +++ b/homeassistant/components/nest/.translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "\u00c8 possibile configurare un solo account Nest.", - "authorize_url_fail": "Errore sconoscioto nel generare l'url di autorizzazione", + "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", "no_flows": "Devi configurare Nest prima di poter eseguire l'autenticazione. [Si prega di leggere le istruzioni] (https://www.home-assistant.io/components/nest/)." }, diff --git a/homeassistant/components/openuv/.translations/es-419.json b/homeassistant/components/openuv/.translations/es-419.json index 6b391c20a0a..332a21f99f5 100644 --- a/homeassistant/components/openuv/.translations/es-419.json +++ b/homeassistant/components/openuv/.translations/es-419.json @@ -7,11 +7,14 @@ "step": { "user": { "data": { + "api_key": "Clave API de OpenUV", "elevation": "Elevaci\u00f3n", "latitude": "Latitud", "longitude": "Longitud" - } + }, + "title": "Completa tu informaci\u00f3n" } - } + }, + "title": "OpenUV" } } \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/it.json b/homeassistant/components/openuv/.translations/it.json index a18d36693d5..82dfd63184a 100644 --- a/homeassistant/components/openuv/.translations/it.json +++ b/homeassistant/components/openuv/.translations/it.json @@ -1,15 +1,20 @@ { "config": { "error": { + "identifier_exists": "Coordinate gi\u00e0 registrate", "invalid_api_key": "Chiave API non valida" }, "step": { "user": { "data": { + "api_key": "API Key di OpenUV", + "elevation": "Altitudine", "latitude": "Latitudine", "longitude": "Logitudine" - } + }, + "title": "Inserisci i tuoi dati" } - } + }, + "title": "OpenUV" } } \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/es-419.json b/homeassistant/components/owntracks/.translations/es-419.json new file mode 100644 index 00000000000..f56cff977d0 --- /dev/null +++ b/homeassistant/components/owntracks/.translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "\n\n En Android, abra [la aplicaci\u00f3n OwnTracks] ( {android_url} ), vaya a preferencias - > conexi\u00f3n. Cambia las siguientes configuraciones: \n - Modo: HTTP privado \n - Anfitri\u00f3n: {webhook_url} \n - Identificaci\u00f3n: \n - Nombre de usuario: ` ` \n - ID del dispositivo: ` ` \n\n En iOS, abra [la aplicaci\u00f3n OwnTracks] ( {ios_url} ), toque el icono (i) en la parte superior izquierda - > configuraci\u00f3n. Cambia las siguientes configuraciones: \n - Modo: HTTP \n - URL: {webhook_url} \n - Activar autenticaci\u00f3n \n - ID de usuario: ` ` \n\n {secret} \n \n Consulte [la documentaci\u00f3n] ( {docs_url} ) para obtener m\u00e1s informaci\u00f3n." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1s seguro de que quieres configurar OwnTracks?", + "title": "Configurar OwnTracks" + } + }, + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/it.json b/homeassistant/components/owntracks/.translations/it.json new file mode 100644 index 00000000000..9b66b693c33 --- /dev/null +++ b/homeassistant/components/owntracks/.translations/it.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare OwnTracks?", + "title": "Configura OwnTracks" + } + }, + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/es-419.json b/homeassistant/components/point/.translations/es-419.json new file mode 100644 index 00000000000..c20e3350272 --- /dev/null +++ b/homeassistant/components/point/.translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "external_setup": "Punto configurado con \u00e9xito desde otro flujo." + }, + "error": { + "follow_link": "Por favor, siga el enlace y autent\u00edquese antes de presionar Enviar", + "no_token": "No autenticado con Minut" + }, + "step": { + "auth": { + "description": "Siga el enlace a continuaci\u00f3n y Aceptar acceso a su cuenta de Minut, luego vuelva y presione Enviar continuaci\u00f3n. \n\n [Enlace] ( {authorization_url} )" + }, + "user": { + "data": { + "flow_impl": "Proveedor" + }, + "description": "Elija a trav\u00e9s de qu\u00e9 proveedor de autenticaci\u00f3n desea autenticarse con Point.", + "title": "Proveedor de autenticaci\u00f3n" + } + }, + "title": "Minut Point" + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/hu.json b/homeassistant/components/point/.translations/hu.json index 2d52069d5ba..3192454550d 100644 --- a/homeassistant/components/point/.translations/hu.json +++ b/homeassistant/components/point/.translations/hu.json @@ -1,7 +1,16 @@ { "config": { + "abort": { + "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n." + }, + "error": { + "follow_link": "K\u00e9rlek, k\u00f6vesd a hivatkoz\u00e1st \u00e9s hiteles\u00edtsd magad miel\u0151tt megnyomod a K\u00fcld\u00e9s gombot", + "no_token": "A Minut nincs hiteles\u00edtve" + }, "step": { "auth": { + "description": "K\u00e9rlek k\u00f6vesd az al\u00e1bbi linket \u00e9s a Fogadd el a hozz\u00e1f\u00e9r\u00e9st a Minut fi\u00f3kj\u00e1hoz, majd t\u00e9rj vissza \u00e9s nyomd meg a K\u00fcld\u00e9s gombot. \n\n [Link] ( {authorization_url} )", "title": "Point hiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/point/.translations/it.json b/homeassistant/components/point/.translations/it.json index 00e2cb02358..324801009ca 100644 --- a/homeassistant/components/point/.translations/it.json +++ b/homeassistant/components/point/.translations/it.json @@ -1,12 +1,31 @@ { "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare un solo account Point.", + "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", + "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", + "external_setup": "Point configurato correttamente da un altro flusso.", + "no_flows": "Devi configurare Point prima di poter eseguire l'autenticazione. [Si prega di leggere le istruzioni] (https://www.home-assistant.io/components/point/)." + }, + "create_entry": { + "default": "Autenticato con successo con Minut per i tuoi dispositivi Point" + }, + "error": { + "follow_link": "Segui il link e autenticati prima di premere Invio", + "no_token": "Non autenticato con Minut" + }, "step": { + "auth": { + "title": "Autenticare Point" + }, "user": { "data": { "flow_impl": "Provider" }, + "description": "Scegli tramite quale provider di autenticazione vuoi autenticarti con Point.", "title": "Provider di autenticazione" } - } + }, + "title": "Minut Point" } } \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/ca.json b/homeassistant/components/ps4/.translations/ca.json new file mode 100644 index 00000000000..350b65ca815 --- /dev/null +++ b/homeassistant/components/ps4/.translations/ca.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Error en l'obtenci\u00f3 de les credencials.", + "devices_configured": "Tots els dispositius trobats ja estan configurats.", + "no_devices_found": "No s'han trobat dispositius PlayStation 4 a la xarxa.", + "port_987_bind_error": "No s'ha pogut vincular amb el port 987.", + "port_997_bind_error": "No s'ha pogut vincular amb el port 997." + }, + "error": { + "login_failed": "No s'ha pogut sincronitzar amb la PlayStation 4. Verifica el codi PIN.", + "not_ready": "La PlayStation 4 no est\u00e0 engegada o no s'ha connectada a la xarxa." + }, + "step": { + "creds": { + "description": "Credencials necess\u00e0ries. Prem 'Enviar' i, a continuaci\u00f3, a la segona pantalla de l'aplicaci\u00f3 de la PS4, actualitza els dispositius i selecciona 'Home-Assistant' per continuar.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "Adre\u00e7a IP", + "name": "Nom", + "region": "Regi\u00f3" + }, + "description": "Introdueix la informaci\u00f3 de la teva PlayStation 4. Pel 'PIN', ves a 'Configuraci\u00f3' de la PlayStation 4, despr\u00e9s navega fins a 'Configuraci\u00f3 de la connexi\u00f3 de l'aplicaci\u00f3 m\u00f2bil' i selecciona 'Afegir dispositiu'. Introdueix el PIN que es mostra.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/cs.json b/homeassistant/components/ps4/.translations/cs.json new file mode 100644 index 00000000000..5c4e67a324c --- /dev/null +++ b/homeassistant/components/ps4/.translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "link": { + "data": { + "region": "Region" + }, + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/da.json b/homeassistant/components/ps4/.translations/da.json new file mode 100644 index 00000000000..7c5f9e7621c --- /dev/null +++ b/homeassistant/components/ps4/.translations/da.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Fejl ved hentning af legitimationsoplysninger.", + "devices_configured": "Alle de fundne enheder er allerede konfigureret.", + "no_devices_found": "Ingen PlayStation 4 enheder fundet p\u00e5 netv\u00e6rket.", + "port_987_bind_error": "Kunne ikke binde til port 987.", + "port_997_bind_error": "Kunne ikke binde til port 997." + }, + "error": { + "login_failed": "Kunne ikke parre med PlayStation 4. Kontroller PIN er korrekt.", + "not_ready": "PlayStation 4 er ikke t\u00e6ndt eller tilsluttet til netv\u00e6rket." + }, + "step": { + "creds": { + "description": "Legitimationsoplysninger er n\u00f8dvendige. Tryk p\u00e5 'Send' og derefter i PS4 2nd Screen App, v\u00e6lg opdater enheder og v\u00e6lg 'Home-Assistant' -enheden for at forts\u00e6tte.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP-adresse", + "name": "Navn", + "region": "Omr\u00e5de" + }, + "description": "Indtast dine PlayStation 4 oplysninger. For 'PIN' skal du navigere til 'Indstillinger' p\u00e5 din PlayStation 4 konsol. G\u00e5 derefter til 'Indstillinger for mobilapp-forbindelse' og v\u00e6lg 'Tilf\u00f8j enhed'. Indtast den PIN der vises.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/en.json b/homeassistant/components/ps4/.translations/en.json index b546280a7ce..c0b476ff4e2 100644 --- a/homeassistant/components/ps4/.translations/en.json +++ b/homeassistant/components/ps4/.translations/en.json @@ -1,32 +1,32 @@ { - "config": { - "title": "PlayStation 4", - "step": { - "creds": { - "title": "PlayStation 4", - "description": "Credentials needed. Press 'Submit' and then in the PS4 2nd Screen App, refresh devices and select the 'Home-Assistant' device to continue." - }, - "link": { - "title": "PlayStation 4", - "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed.", - "data": { - "region": "Region", - "name": "Name", - "code": "PIN", - "ip_address": "IP Address" - } - } - }, - "error": { - "not_ready": "PlayStation 4 is not on or connected to network.", - "login_failed": "Failed to pair to PlayStation 4. Verify PIN is correct." - }, - "abort": { - "credential_error": "Error fetching credentials.", - "no_devices_found": "No PlayStation 4 devices found on the network.", - "devices_configured": "All devices found are already configured.", - "port_987_bind_error": "Could not bind to UDP port 987. Port in use or additional configuration required. See component documentation at: https://home-assistant.io/components/media_player.ps4/", - "port_997_bind_error": "Could not bind to TCP port 997. Port in use or additional configuration required. See component documentation at: https://home-assistant.io/components/media_player.ps4/" + "config": { + "abort": { + "credential_error": "Error fetching credentials.", + "devices_configured": "All devices found are already configured.", + "no_devices_found": "No PlayStation 4 devices found on the network.", + "port_987_bind_error": "Could not bind to port 987.", + "port_997_bind_error": "Could not bind to port 997." + }, + "error": { + "login_failed": "Failed to pair to PlayStation 4. Verify PIN is correct.", + "not_ready": "PlayStation 4 is not on or connected to network." + }, + "step": { + "creds": { + "description": "Credentials needed. Press 'Submit' and then in the PS4 2nd Screen App, refresh devices and select the 'Home-Assistant' device to continue.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP Address", + "name": "Name", + "region": "Region" + }, + "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/es-419.json b/homeassistant/components/ps4/.translations/es-419.json new file mode 100644 index 00000000000..093ee552951 --- /dev/null +++ b/homeassistant/components/ps4/.translations/es-419.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Error al obtener las credenciales.", + "devices_configured": "Todos los dispositivos encontrados ya est\u00e1n configurados.", + "no_devices_found": "No se encontraron dispositivos PlayStation 4 en la red.", + "port_987_bind_error": "No se pudo enlazar al puerto 987.", + "port_997_bind_error": "No se pudo enlazar al puerto 997." + }, + "error": { + "login_failed": "No se ha podido emparejar con PlayStation 4. Verifique que el PIN sea correcto.", + "not_ready": "PlayStation 4 no est\u00e1 encendida o conectada a la red." + }, + "step": { + "creds": { + "description": "Credenciales necesarias. Presione 'Enviar' y luego en la aplicaci\u00f3n de la segunda pantalla de PS4, actualice los dispositivos y seleccione el dispositivo 'Home-Assistant' para continuar.", + "title": "Playstation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "Direcci\u00f3n IP", + "name": "Nombre", + "region": "Regi\u00f3n" + }, + "description": "Ingresa tu informaci\u00f3n de PlayStation 4. Para 'PIN', navegue hasta 'Configuraci\u00f3n' en su consola PlayStation 4. Luego navegue a 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y seleccione 'Agregar dispositivo'. Ingrese el PIN que se muestra.", + "title": "Playstation 4" + } + }, + "title": "Playstation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/it.json b/homeassistant/components/ps4/.translations/it.json new file mode 100644 index 00000000000..5e83d7bd39c --- /dev/null +++ b/homeassistant/components/ps4/.translations/it.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Errore nel recupero delle credenziali.", + "devices_configured": "Tutti i dispositivi trovati sono gi\u00e0 configurati.", + "no_devices_found": "Nessun dispositivo PlayStation 4 trovato in rete.", + "port_987_bind_error": "Impossibile connettersi alla porta 987.", + "port_997_bind_error": "Impossibile connettersi alla porta 997." + }, + "error": { + "login_failed": "Accoppiamento alla PlayStation 4 fallito. Verifica che il PIN sia corretto.", + "not_ready": "La PlayStation 4 non \u00e8 accesa o non \u00e8 collegata alla rete." + }, + "step": { + "creds": { + "description": "Credenziali necessarie. Premi 'Invia' e poi, nella seconda schermata della App PS4, aggiorna i dispositivi e seleziona il dispositivo 'Home-Assistant' per continuare.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "Indirizzo IP", + "name": "Nome", + "region": "Area geografica" + }, + "description": "Inserisci le informazioni della tua PlayStation 4. Per il \"PIN\", vai su \"Impostazioni\" sulla tua console PlayStation 4. Quindi accedi a \"Impostazioni connessione app mobile\" e seleziona \"Aggiungi dispositivo\". Inserisci il PIN che viene visualizzato.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/ko.json b/homeassistant/components/ps4/.translations/ko.json new file mode 100644 index 00000000000..019fbef1ef3 --- /dev/null +++ b/homeassistant/components/ps4/.translations/ko.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "\uc790\uaca9 \uc99d\uba85\uc744 \uac00\uc838\uc624\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "devices_configured": "\ubc1c\uacac \ub41c \ubaa8\ub4e0 \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "no_devices_found": "PlayStation 4 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "port_987_bind_error": "\ud3ec\ud2b8 987 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "port_997_bind_error": "\ud3ec\ud2b8 997 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "error": { + "login_failed": "PlayStation 4 \uc640 \ud398\uc5b4\ub9c1\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. PIN \uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "not_ready": "PlayStation 4 \uac00 \ucf1c\uc838 \uc788\uc9c0 \uc54a\uac70\ub098 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." + }, + "step": { + "creds": { + "description": "\uc790\uaca9 \uc99d\uba85\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. 'Submit' \uc744 \ub204\ub978 \ub2e4\uc74c PS4 Second Screen \uc571\uc5d0\uc11c \uae30\uae30\ub97c \uc0c8\ub85c \uace0\uce68\ud558\uace0 'Home-Assistant' \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP \uc8fc\uc18c", + "name": "\uc774\ub984", + "region": "\uc9c0\uc5ed" + }, + "description": "PlayStation 4 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. 'PIN' \uc744 \ud655\uc778\ud558\ub824\uba74, PlayStation 4 \ucf58\uc194\uc5d0\uc11c '\uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud55c \ub4a4 '\ubaa8\ubc14\uc77c \uc571 \uc5f0\uacb0 \uc124\uc815'\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec '\uae30\uae30 \ucd94\uac00'\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ud45c\uc2dc\ub41c PIN\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/lb.json b/homeassistant/components/ps4/.translations/lb.json new file mode 100644 index 00000000000..15b90cb6b6b --- /dev/null +++ b/homeassistant/components/ps4/.translations/lb.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Feeler beim ausliesen vun den Umeldungs Informatiounen.", + "devices_configured": "All Apparater sinn schonn konfigur\u00e9iert", + "no_devices_found": "Keng Playstation 4 am Netzwierk fonnt.", + "port_987_bind_error": "Konnt sech net mam Port 987 verbannen.", + "port_997_bind_error": "Konnt sech net mam Port 997 verbannen." + }, + "error": { + "login_failed": "Feeler beim verbanne mat der Playstation 4. Iwwerpr\u00e9ift op de PIN korrekt ass.", + "not_ready": "PlayStation 4 ass net un oder mam Netzwierk verbonnen." + }, + "step": { + "creds": { + "description": "Umeldungsinformatioun sinn n\u00e9ideg. Dr\u00e9ckt op 'Ofsch\u00e9cken' , dann an der PS4 App, 2ten Ecran, erneiert Apparater an wielt den Home-Assistant Apparat aus fir weider ze fueren.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP Adresse", + "name": "Numm", + "region": "Regioun" + }, + "description": "Gitt \u00e4r Playstation 4 Informatiounen an. Fir 'PIN', gitt an d'Astellunge vun der Playstation 4 Konsole. Dann op 'Mobile App Verbindungs Astellungen' a wielt \"Apparat dob\u00e4isetzen' aus. Gitt de PIN an deen ugewise g\u00ebtt.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/pl.json b/homeassistant/components/ps4/.translations/pl.json new file mode 100644 index 00000000000..eea4eda0810 --- /dev/null +++ b/homeassistant/components/ps4/.translations/pl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "B\u0142\u0105d podczas pobierania danych logowania.", + "devices_configured": "Wszystkie znalezione urz\u0105dzenia s\u0105 ju\u017c skonfigurowane.", + "no_devices_found": "W sieci nie znaleziono urz\u0105dze\u0144 PlayStation 4.", + "port_987_bind_error": "Nie mo\u017cna powi\u0105za\u0107 z portem 987.", + "port_997_bind_error": "Nie mo\u017cna powi\u0105za\u0107 z portem 997." + }, + "error": { + "login_failed": "Nie uda\u0142o si\u0119 sparowa\u0107 z PlayStation 4. Sprawd\u017a, czy PIN jest poprawny.", + "not_ready": "PlayStation 4 nie jest w\u0142\u0105czona lub po\u0142\u0105czona z sieci\u0105." + }, + "step": { + "creds": { + "description": "Wymagane s\u0105 po\u015bwiadczenia. Naci\u015bnij przycisk 'Prze\u015blij', a nast\u0119pnie w aplikacji PS4 Second Screen, od\u015bwie\u017c urz\u0105dzenia i wybierz urz\u0105dzenie 'Home-Assistant', aby kontynuowa\u0107.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "Adres IP", + "name": "Nazwa", + "region": "Region" + }, + "description": "Wprowad\u017a informacje o PlayStation 4. Aby uzyska\u0107 'PIN', przejd\u017a do 'Ustawienia' na konsoli PlayStation 4. Nast\u0119pnie przejd\u017a do 'Ustawienia po\u0142\u0105czenia aplikacji mobilnej' i wybierz 'Dodaj urz\u0105dzenie'. Wprowad\u017a wy\u015bwietlony kod PIN.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/pt.json b/homeassistant/components/ps4/.translations/pt.json new file mode 100644 index 00000000000..34a5ebfc4db --- /dev/null +++ b/homeassistant/components/ps4/.translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "creds": { + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "name": "Nome", + "region": "Regi\u00e3o" + }, + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/ru.json b/homeassistant/components/ps4/.translations/ru.json new file mode 100644 index 00000000000..41232ddc2d4 --- /dev/null +++ b/homeassistant/components/ps4/.translations/ru.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445.", + "devices_configured": "\u0412\u0441\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", + "no_devices_found": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 PlayStation 4.", + "port_987_bind_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0440\u0442\u0443 987.", + "port_997_bind_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0440\u0442\u0443 997." + }, + "error": { + "login_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 PlayStation 4. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e PIN-\u043a\u043e\u0434 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439.", + "not_ready": "PlayStation 4 \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0438\u043b\u0438 \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043a \u0441\u0435\u0442\u0438." + }, + "step": { + "creds": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**, \u0430 \u0437\u0430\u0442\u0435\u043c \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 'PS4 Second Screen' \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e 'Home-Assistant', \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN-\u043a\u043e\u0434", + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + }, + "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f PIN-\u043a\u043e\u0434\u0430 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043f\u0443\u043d\u043a\u0442\u0443 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 PlayStation 4. \u0417\u0430\u0442\u0435\u043c \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f** \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e**.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/zh-Hant.json b/homeassistant/components/ps4/.translations/zh-Hant.json new file mode 100644 index 00000000000..b8083eba4ac --- /dev/null +++ b/homeassistant/components/ps4/.translations/zh-Hant.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "\u53d6\u5f97\u6191\u8b49\u932f\u8aa4\u3002", + "devices_configured": "\u6240\u6709\u88dd\u7f6e\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 PlayStation 4 \u8a2d\u5099\u3002", + "port_987_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 987\u3002", + "port_997_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 997\u3002" + }, + "error": { + "login_failed": "PlayStation 4 \u914d\u5c0d\u5931\u6557\uff0c\u8acb\u78ba\u8a8d PIN \u78bc\u3002", + "not_ready": "PlayStation 4 \u4e26\u672a\u958b\u555f\u6216\u672a\u9023\u7dda\u81f3\u7db2\u8def\u3002" + }, + "step": { + "creds": { + "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u88dd\u7f6e\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP \u4f4d\u5740", + "name": "\u540d\u7a31", + "region": "\u5340\u57df" + }, + "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u88dd\u7f6e\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/es-419.json b/homeassistant/components/rainmachine/.translations/es-419.json new file mode 100644 index 00000000000..2cb49dc0ac1 --- /dev/null +++ b/homeassistant/components/rainmachine/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Cuenta ya registrada", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "step": { + "user": { + "data": { + "ip_address": "Nombre de host o direcci\u00f3n IP", + "password": "Contrase\u00f1a", + "port": "Puerto" + }, + "title": "Completa tu informaci\u00f3n" + } + }, + "title": "RainMachine" + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/hu.json b/homeassistant/components/rainmachine/.translations/hu.json index ff98eccbe5a..0f5b6b71126 100644 --- a/homeassistant/components/rainmachine/.translations/hu.json +++ b/homeassistant/components/rainmachine/.translations/hu.json @@ -1,6 +1,7 @@ { "config": { "error": { + "identifier_exists": "A fi\u00f3k m\u00e1r regisztr\u00e1lt", "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok" }, "step": { @@ -9,7 +10,8 @@ "ip_address": "Kiszolg\u00e1l\u00f3 neve vagy IP c\u00edme", "password": "Jelsz\u00f3", "port": "Port" - } + }, + "title": "T\u00f6ltsd ki az adataid" } }, "title": "Rainmachine" diff --git a/homeassistant/components/rainmachine/.translations/it.json b/homeassistant/components/rainmachine/.translations/it.json new file mode 100644 index 00000000000..40b49a926c7 --- /dev/null +++ b/homeassistant/components/rainmachine/.translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Account gi\u00e0 registrato", + "invalid_credentials": "Credenziali non valide" + }, + "step": { + "user": { + "data": { + "ip_address": "Nome dell'host o indirizzo IP", + "password": "Password", + "port": "Porta" + }, + "title": "Inserisci i tuoi dati" + } + }, + "title": "RainMachine" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.it.json b/homeassistant/components/sensor/.translations/moon.it.json index f22a6d340ae..39c7f22f7af 100644 --- a/homeassistant/components/sensor/.translations/moon.it.json +++ b/homeassistant/components/sensor/.translations/moon.it.json @@ -3,7 +3,7 @@ "first_quarter": "Primo quarto", "full_moon": "Luna piena", "last_quarter": "Ultimo quarto", - "new_moon": "Nuova luna", + "new_moon": "Luna nuova", "waning_crescent": "Luna calante", "waning_gibbous": "Gibbosa calante", "waxing_crescent": "Luna crescente", diff --git a/homeassistant/components/simplisafe/.translations/es-419.json b/homeassistant/components/simplisafe/.translations/es-419.json new file mode 100644 index 00000000000..709d045c348 --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Cuenta ya registrada", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "step": { + "user": { + "data": { + "code": "C\u00f3digo (para Home Assistant)", + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Completa tu informaci\u00f3n" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/ca.json b/homeassistant/components/smartthings/.translations/ca.json index 3c0ca05a8d5..1f27e781ee3 100644 --- a/homeassistant/components/smartthings/.translations/ca.json +++ b/homeassistant/components/smartthings/.translations/ca.json @@ -7,7 +7,8 @@ "token_already_setup": "El testimoni d'autenticaci\u00f3 ja ha estat configurat.", "token_forbidden": "El testimoni d'autenticaci\u00f3 no t\u00e9 cont\u00e9 els apartats OAuth obligatoris.", "token_invalid_format": "El testimoni d'autenticaci\u00f3 ha d'estar en format UID/GUID", - "token_unauthorized": "El testimoni d'autenticaci\u00f3 no \u00e9s v\u00e0lid o ja no t\u00e9 autoritzaci\u00f3." + "token_unauthorized": "El testimoni d'autenticaci\u00f3 no \u00e9s v\u00e0lid o ja no t\u00e9 autoritzaci\u00f3.", + "webhook_error": "SmartThings no ha pogut validar l'adre\u00e7a final configurada a `base_url`. Revisa els [requisits del component]({component_url})." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/da.json b/homeassistant/components/smartthings/.translations/da.json index 1c571b4e639..18412069394 100644 --- a/homeassistant/components/smartthings/.translations/da.json +++ b/homeassistant/components/smartthings/.translations/da.json @@ -7,7 +7,8 @@ "token_already_setup": "Token er allerede konfigureret.", "token_forbidden": "Adgangstoken er ikke indenfor OAuth", "token_invalid_format": "Adgangstoken skal v\u00e6re i UID/GUID format", - "token_unauthorized": "Adgangstoken er ugyldigt eller ikke l\u00e6ngere godkendt." + "token_unauthorized": "Adgangstoken er ugyldigt eller ikke l\u00e6ngere godkendt.", + "webhook_error": "SmartThings kunne ikke validere slutpunktet konfigureret i `base_url`. Gennemg\u00e5 venligst komponentkravene." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/es-419.json b/homeassistant/components/smartthings/.translations/es-419.json new file mode 100644 index 00000000000..4dc94324695 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/es-419.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "app_setup_error": "No se puede configurar el SmartApp. Por favor, int\u00e9ntelo de nuevo.", + "base_url_not_https": "El `base_url` para el componente `http` debe estar configurado y empezar por `https://`.", + "token_already_setup": "El token ya ha sido configurado.", + "token_invalid_format": "El token debe estar en formato UID/GUID", + "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado.", + "webhook_error": "SmartThings no pudo validar el endpoint configurado en `base_url`. Por favor, revise los requisitos de los componentes." + }, + "step": { + "user": { + "data": { + "access_token": "Token de acceso" + }, + "title": "Ingresar token de acceso personal" + }, + "wait_install": { + "description": "Instale la SmartApp de Home Assistant en al menos una ubicaci\u00f3n y haga clic en enviar.", + "title": "Instalar SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/hu.json b/homeassistant/components/smartthings/.translations/hu.json new file mode 100644 index 00000000000..e4970780bc0 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/hu.json @@ -0,0 +1,28 @@ +{ + "config": { + "error": { + "app_not_installed": "Gy\u0151z\u0151dj meg r\u00f3la, hogy telep\u00edtetted \u00e9s enged\u00e9lyezted a SmartApp Home Assistant alkalmaz\u00e1st, \u00e9s pr\u00f3b\u00e1lkozz \u00fajra.", + "app_setup_error": "A SmartApp be\u00e1ll\u00edt\u00e1sa nem siker\u00fclt. K\u00e9rlek pr\u00f3b\u00e1ld \u00fajra.", + "base_url_not_https": "A `http` \u00f6sszetev\u0151 `base_url` be\u00e1ll\u00edt\u00e1s\u00e1t konfigur\u00e1lni kell, \u00e9s `https: //` -vel kell kezdeni.", + "token_already_setup": "A tokent m\u00e1r be\u00e1ll\u00edtottuk.", + "token_forbidden": "A token nem rendelkezik a sz\u00fcks\u00e9ges OAuth-tartom\u00e1nyokkal.", + "token_invalid_format": "A tokennek UID / GUID form\u00e1tumban kell lennie", + "token_unauthorized": "A token \u00e9rv\u00e9nytelen vagy m\u00e1r nem enged\u00e9lyezett.", + "webhook_error": "A SmartThings nem tudta \u00e9rv\u00e9nyes\u00edteni a `base_url`-ben konfigur\u00e1lt v\u00e9gpontot. K\u00e9rlek, tekintsd \u00e1t az \u00f6sszetev\u0151 k\u00f6vetelm\u00e9nyeit." + }, + "step": { + "user": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9s a Tokenhez" + }, + "description": "K\u00e9rlek add meg a SmartThings [Personal Access Tokent]({token_url}), amit az [instrukci\u00f3k] ({component_url}) alapj\u00e1n hozt\u00e1l l\u00e9tre.", + "title": "Adja meg a szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si Tokent" + }, + "wait_install": { + "description": "K\u00e9rj\u00fck, telep\u00edtse a Home Assistant SmartAppot legal\u00e1bb egy helyre, \u00e9s kattintson a K\u00fcld\u00e9s gombra.", + "title": "A SmartApp telep\u00edt\u00e9se" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/it.json b/homeassistant/components/smartthings/.translations/it.json new file mode 100644 index 00000000000..486a61847a7 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "app_not_installed": "Assicurati di avere installato ed autorizzato la SmartApp Home Assistant e riprova.", + "app_setup_error": "Impossibile configurare SmartApp. Riprovare.", + "base_url_not_https": "Il `base_url` per il componente `http` deve essere configurato e deve iniziare con `https://`.", + "token_already_setup": "Il token \u00e8 gi\u00e0 stato configurato.", + "token_invalid_format": "Il token deve essere nel formato UID/GUID", + "token_unauthorized": "Il token non \u00e8 valido o non \u00e8 pi\u00f9 autorizzato.", + "webhook_error": "SmartThings non ha potuto convalidare l'endpoint configurato in `base_url`. Si prega di rivedere i requisiti del componente." + }, + "step": { + "user": { + "data": { + "access_token": "Token di accesso" + }, + "description": "Inserisci un [Token di Accesso Personale]({token_url}) di SmartThings che \u00e8 stato creato secondo lo [istruzioni]({component_url}).", + "title": "Inserisci il Token di Accesso Personale" + }, + "wait_install": { + "title": "Installa SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/ko.json b/homeassistant/components/smartthings/.translations/ko.json index e4131543d50..f7d86af8394 100644 --- a/homeassistant/components/smartthings/.translations/ko.json +++ b/homeassistant/components/smartthings/.translations/ko.json @@ -3,11 +3,12 @@ "error": { "app_not_installed": "Home Assistant SmartApp \uc744 \uc124\uce58\ud558\uace0 \uc778\uc99d\ud588\ub294\uc9c0 \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "app_setup_error": "SmartApp \uc744 \uc124\uc815\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "base_url_not_https": "`http` \uad6c\uc131\uc694\uc18c\ub97c \uc704\ud55c `base_url` \uc740 `https://`\ub85c \uc2dc\uc791\ud558\ub3c4\ub85d \uad6c\uc131\ub418\uc5b4\uc57c\ud569\ub2c8\ub2e4.", + "base_url_not_https": "`http` \uad6c\uc131\uc694\uc18c\uc758 `base_url` \uc740 \ubc18\ub4dc\uc2dc `https://`\ub85c \uc2dc\uc791\ud558\ub3c4\ub85d \uad6c\uc131\ub418\uc5b4 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.", "token_already_setup": "\ud1a0\ud070\uc774 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "token_forbidden": "\ud1a0\ud070\uc5d0 \ud544\uc694\ud55c OAuth \ubc94\uc704\ubaa9\ub85d\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.", - "token_invalid_format": "\ud1a0\ud070\uc740 UID/GUID \ud615\uc2dd\uc774\uc5b4\uc57c\ud569\ub2c8\ub2e4", - "token_unauthorized": "\ud1a0\ud070\uc774 \uc720\ud6a8\ud558\uc9c0 \uc54a\uac70\ub098 \uc2b9\uc778\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4." + "token_invalid_format": "\ud1a0\ud070\uc740 UID/GUID \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4", + "token_unauthorized": "\ud1a0\ud070\uc774 \uc720\ud6a8\ud558\uc9c0 \uc54a\uac70\ub098 \uc2b9\uc778\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "webhook_error": "SmartThings \ub294 `base_url` \uc5d0 \uc124\uc815\ub41c \uc5d4\ub4dc\ud3ec\uc778\ud2b8\uc758 \uc720\ud6a8\uc131\uc744 \uac80\uc0ac \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uad6c\uc131\uc694\uc18c\uc758 \uc694\uad6c \uc0ac\ud56d\uc744 \uac80\ud1a0\ud574\uc8fc\uc138\uc694." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/lb.json b/homeassistant/components/smartthings/.translations/lb.json index fd59d187314..fc80ba9f722 100644 --- a/homeassistant/components/smartthings/.translations/lb.json +++ b/homeassistant/components/smartthings/.translations/lb.json @@ -7,7 +7,8 @@ "token_already_setup": "Den Jeton gouf schonn ageriicht.", "token_forbidden": "De Jeton huet net d\u00e9i n\u00e9ideg OAuth M\u00e9iglechkeeten.", "token_invalid_format": "De Jeton muss am UID/GUID Format sinn", - "token_unauthorized": "De Jeton ass ong\u00eblteg oder net m\u00e9i autoris\u00e9iert." + "token_unauthorized": "De Jeton ass ong\u00eblteg oder net m\u00e9i autoris\u00e9iert.", + "webhook_error": "SmartThings konnt den an der 'base_url' defin\u00e9ierten Endpoint net valid\u00e9ieren. Iwwerpr\u00e9ift d'Viraussetzunge vun d\u00ebser Komponente" }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/pl.json b/homeassistant/components/smartthings/.translations/pl.json index 570f1130383..33803994764 100644 --- a/homeassistant/components/smartthings/.translations/pl.json +++ b/homeassistant/components/smartthings/.translations/pl.json @@ -7,7 +7,8 @@ "token_already_setup": "Token zosta\u0142 ju\u017c skonfigurowany.", "token_forbidden": "Token nie ma wymaganych zakres\u00f3w OAuth.", "token_invalid_format": "Token musi by\u0107 w formacie UID/GUID", - "token_unauthorized": "Token jest niewa\u017cny lub nie ma ju\u017c autoryzacji." + "token_unauthorized": "Token jest niewa\u017cny lub nie ma ju\u017c autoryzacji.", + "webhook_error": "SmartThings nie mo\u017ce sprawdzi\u0107 poprawno\u015bci punktu ko\u0144cowego skonfigurowanego w `base_url`. Sprawd\u017a wymagania dotycz\u0105ce komponentu." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/pt.json b/homeassistant/components/smartthings/.translations/pt.json index d805cfc563d..f49fe04ae8e 100644 --- a/homeassistant/components/smartthings/.translations/pt.json +++ b/homeassistant/components/smartthings/.translations/pt.json @@ -1,16 +1,24 @@ { "config": { "error": { + "app_not_installed": "Por favor, instale o Home Assistant SmartApp em pelo menos um local e tente de novo.", "app_setup_error": "N\u00e3o \u00e9 poss\u00edvel configurar o SmartApp. Por favor, tente novamente.", - "token_already_setup": "O token j\u00e1 foi configurado." + "base_url_not_https": "O `base_url` para o componente` http` deve ser configurado e iniciar com `https://`.", + "token_already_setup": "O token j\u00e1 foi configurado.", + "token_forbidden": "O token n\u00e3o tem tem a cobertura OAuth necess\u00e1ria.", + "token_invalid_format": "O token deve estar no formato UID/GUID", + "token_unauthorized": "O token \u00e9 inv\u00e1lido ou ja n\u00e3o est\u00e1 autorizado." }, "step": { "user": { "data": { "access_token": "Token de Acesso" - } + }, + "description": "Por favor, insira um SmartThings [Personal Access Token]({token_url} ) que foi criado de acordo com as [instru\u00e7\u00f5es]({component_url}).", + "title": "Insira o Token de acesso pessoal" }, "wait_install": { + "description": "Por favor, instale o Home Assistant SmartApp em pelo menos um local e clique em enviar.", "title": "Instalar SmartApp" } }, diff --git a/homeassistant/components/smartthings/.translations/ru.json b/homeassistant/components/smartthings/.translations/ru.json index 334e5d8cb23..6e34cf8a49a 100644 --- a/homeassistant/components/smartthings/.translations/ru.json +++ b/homeassistant/components/smartthings/.translations/ru.json @@ -1,13 +1,14 @@ { "config": { "error": { - "app_not_installed": "\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b\u0438 \u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043b\u0438 SmartApp Home Assistant \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", - "app_setup_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c SmartApp. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "app_not_installed": "\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b\u0438 \u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043b\u0438 SmartApp 'Home Assistant' \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "app_setup_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c SmartApp. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", "base_url_not_https": "\u0412 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0435 `http` \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 `base_url`, \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u0441 `https://`.", "token_already_setup": "\u0422\u043e\u043a\u0435\u043d \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", "token_forbidden": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f OAuth.", "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 UID / GUID", - "token_unauthorized": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u043b\u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d." + "token_unauthorized": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u043b\u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d.", + "webhook_error": "SmartThings \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443, \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u0432 `base_url`. \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\u0435\u0439 \u043a \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0443." }, "step": { "user": { @@ -18,7 +19,7 @@ "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" }, "wait_install": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 SmartApp Home Assistant \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**.", + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 SmartApp 'Home Assistant' \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**.", "title": "SmartThings" } }, diff --git a/homeassistant/components/smartthings/.translations/sl.json b/homeassistant/components/smartthings/.translations/sl.json new file mode 100644 index 00000000000..e274d8c9394 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/sl.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "app_not_installed": "Prepri\u010dajte se, da ste namestili in pooblastili Home Assistant SmartApp in poskusite znova.", + "app_setup_error": "SmartApp ni mogo\u010de nastaviti. Prosim poskusite ponovno.", + "base_url_not_https": "`Base_url` za` http 'komponento je treba konfigurirati in za\u010deti z `https: //`.", + "token_already_setup": "\u017deton je \u017ee nastavljen.", + "token_forbidden": "\u017deton nima zahtevanih OAuth obsegov.", + "token_invalid_format": "\u017deton mora biti v formatu UID / GUID", + "token_unauthorized": "\u017deton ni veljaven ali ni ve\u010d poobla\u0161\u010den." + }, + "step": { + "user": { + "data": { + "access_token": "\u017deton za dostop" + }, + "description": "Prosimo vnesite Smartthings [\u017deton za osebni dostop]({token_url}) ki je bil kreiran v skladu z [navodili]({component_url}).", + "title": "Vnesite \u017eeton za osebni dostop" + }, + "wait_install": { + "description": "Prosimo, namestite Home Assistant SmartApp v vsaj eni lokaciji in kliknite po\u0161lji.", + "title": "Namesti SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/zh-Hant.json b/homeassistant/components/smartthings/.translations/zh-Hant.json index 952eafec60c..10d73f8be35 100644 --- a/homeassistant/components/smartthings/.translations/zh-Hant.json +++ b/homeassistant/components/smartthings/.translations/zh-Hant.json @@ -7,7 +7,8 @@ "token_already_setup": "\u5bc6\u9470\u5df2\u8a2d\u5b9a\u904e\u3002", "token_forbidden": "\u5bc6\u9470\u4e0d\u5177\u6240\u9700\u7684 OAuth \u7bc4\u570d\u3002", "token_invalid_format": "\u5bc6\u9470\u5fc5\u9808\u70ba UID/GUID \u683c\u5f0f", - "token_unauthorized": "\u5bc6\u9470\u7121\u6548\u6216\u4e0d\u518d\u5177\u6709\u6388\u6b0a\u3002" + "token_unauthorized": "\u5bc6\u9470\u7121\u6548\u6216\u4e0d\u518d\u5177\u6709\u6388\u6b0a\u3002", + "webhook_error": "SmartThings \u7121\u6cd5\u8a8d\u8b49\u300cbase_url\u300d\u4e2d\u8a2d\u5b9a\u4e4b\u7aef\u9ede\u3002\u8acb\u518d\u6b21\u78ba\u8a8d\u5143\u4ef6\u9700\u6c42\u3002" }, "step": { "user": { diff --git a/homeassistant/components/smhi/.translations/es-419.json b/homeassistant/components/smhi/.translations/es-419.json new file mode 100644 index 00000000000..a3fb9ee5e27 --- /dev/null +++ b/homeassistant/components/smhi/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "El nombre ya existe", + "wrong_location": "Ubicaci\u00f3n Suecia solamente" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "title": "Ubicaci\u00f3n en Suecia" + } + }, + "title": "Servicio meteorol\u00f3gico sueco (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/hu.json b/homeassistant/components/smhi/.translations/hu.json index 425cf927631..8c79ff3bfaf 100644 --- a/homeassistant/components/smhi/.translations/hu.json +++ b/homeassistant/components/smhi/.translations/hu.json @@ -13,6 +13,7 @@ }, "title": "Helysz\u00edn Sv\u00e9dorsz\u00e1gban" } - } + }, + "title": "Sv\u00e9d Meteorol\u00f3giai Szolg\u00e1lat (SMHI)" } } \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/it.json b/homeassistant/components/smhi/.translations/it.json new file mode 100644 index 00000000000..b8c228f7e9e --- /dev/null +++ b/homeassistant/components/smhi/.translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Il nome \u00e8 gi\u00e0 esistente", + "wrong_location": "Localit\u00e0 solamente della Svezia" + }, + "step": { + "user": { + "data": { + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome" + }, + "title": "Localit\u00e0 in Svezia" + } + }, + "title": "Servizio meteo svedese (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/it.json b/homeassistant/components/sonos/.translations/it.json index e32557f1d95..06c873b436e 100644 --- a/homeassistant/components/sonos/.translations/it.json +++ b/homeassistant/components/sonos/.translations/it.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Vuoi installare Sonos", + "description": "Vuoi configurare Sonos?", "title": "Sonos" } }, diff --git a/homeassistant/components/tellduslive/.translations/es-419.json b/homeassistant/components/tellduslive/.translations/es-419.json new file mode 100644 index 00000000000..bf74d104835 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/es-419.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "all_configured": "TelldusLive ya est\u00e1 configurado", + "already_setup": "TelldusLive ya est\u00e1 configurado", + "unknown": "Se produjo un error desconocido" + }, + "error": { + "auth_error": "Error de autenticaci\u00f3n, por favor intente de nuevo" + }, + "step": { + "auth": { + "description": "Para vincular su cuenta de TelldusLive: \n 1. Haga clic en el siguiente enlace \n 2. Inicia sesi\u00f3n en Telldus Live \n 3. Autorice ** {app_name} ** (haga clic en ** S\u00ed **). \n 4. Vuelve aqu\u00ed y haz clic en ** ENVIAR **. \n\n [Enlace a la cuenta de TelldusLive] ( {auth_url} )", + "title": "Autenticar con TelldusLive" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/hu.json b/homeassistant/components/tellduslive/.translations/hu.json index ffa983db093..6057d7b3212 100644 --- a/homeassistant/components/tellduslive/.translations/hu.json +++ b/homeassistant/components/tellduslive/.translations/hu.json @@ -1,15 +1,24 @@ { "config": { "abort": { + "all_configured": "A TelldusLive-ot m\u00e1r be\u00e1ll\u00edtottuk.", + "already_setup": "A TelldusLive m\u00e1r be van \u00e1ll\u00edtva", + "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" }, + "error": { + "auth_error": "Hiteles\u00edt\u00e9si hiba, pr\u00f3b\u00e1lkozz \u00fajra" + }, "step": { "user": { "data": { "host": "Kiszolg\u00e1l\u00f3" }, - "description": "\u00dcres" + "description": "\u00dcres", + "title": "V\u00e1lassz v\u00e9gpontot." } - } + }, + "title": "Telldus Live" } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/it.json b/homeassistant/components/tellduslive/.translations/it.json new file mode 100644 index 00000000000..90f13184a67 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "TelldusLive \u00e8 gi\u00e0 configurato", + "already_setup": "TelldusLive \u00e8 gi\u00e0 configurato", + "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", + "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", + "unknown": "Si \u00e8 verificato un errore sconosciuto." + }, + "error": { + "auth_error": "Errore di autenticazione, riprovare" + }, + "step": { + "auth": { + "title": "Autenticati con TelldusLive" + }, + "user": { + "data": { + "host": "Host" + }, + "title": "Scegli l'endpoint." + } + }, + "title": "Telldus Live" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/pt.json b/homeassistant/components/tellduslive/.translations/pt.json index 90da12451df..a13f71f7505 100644 --- a/homeassistant/components/tellduslive/.translations/pt.json +++ b/homeassistant/components/tellduslive/.translations/pt.json @@ -2,10 +2,14 @@ "config": { "abort": { "all_configured": "TelldusLive j\u00e1 est\u00e1 configurado", + "already_setup": "TelldusLive j\u00e1 est\u00e1 configurado", "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", "unknown": "Ocorreu um erro desconhecido" }, + "error": { + "auth_error": "Erro de autentica\u00e7\u00e3o, por favor tente novamente" + }, "step": { "auth": { "description": "Para ligar \u00e0 sua conta do TelldusLive: \n 1. Clique no link abaixo \n 2. Fa\u00e7a o login no Telldus Live \n 3. Autorize **{app_name}** (clique em **Sim**). \n 4. Volte aqui e clique em **ENVIAR**. \n\n [Ligar \u00e0 TelldusLive] ( {auth_url} )", diff --git a/homeassistant/components/tellduslive/.translations/zh-Hant.json b/homeassistant/components/tellduslive/.translations/zh-Hant.json index c632b543634..c95e96b21c9 100644 --- a/homeassistant/components/tellduslive/.translations/zh-Hant.json +++ b/homeassistant/components/tellduslive/.translations/zh-Hant.json @@ -20,7 +20,7 @@ "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u7a7a\u767d", - "title": "\u9078\u64c7 endpoint\u3002" + "title": "\u9078\u64c7\u7aef\u9ede\u3002" } }, "title": "Telldus Live" diff --git a/homeassistant/components/tplink/.translations/en.json b/homeassistant/components/tplink/.translations/en.json index e353c1363ab..ff349fe1b68 100644 --- a/homeassistant/components/tplink/.translations/en.json +++ b/homeassistant/components/tplink/.translations/en.json @@ -1,15 +1,15 @@ { - "config": { - "title": "TP-Link Smart Home", - "step": { - "confirm": { - "title": "TP-Link Smart Home", - "description": "Do you want to setup TP-Link smart devices?" - } - }, - "abort": { - "single_instance_allowed": "Only a single configuration is necessary.", - "no_devices_found": "No TP-Link devices found on the network." + "config": { + "abort": { + "no_devices_found": "No TP-Link devices found on the network.", + "single_instance_allowed": "Only a single configuration is necessary." + }, + "step": { + "confirm": { + "description": "Do you want to setup TP-Link smart devices?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/es-419.json b/homeassistant/components/tradfri/.translations/es-419.json new file mode 100644 index 00000000000..55016606e2d --- /dev/null +++ b/homeassistant/components/tradfri/.translations/es-419.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El Bridge ya est\u00e1 configurado" + }, + "error": { + "invalid_key": "Error al registrarse con la clave proporcionada. Si esto sigue sucediendo, intente reiniciar el gateway.", + "timeout": "Tiempo de espera para validar el c\u00f3digo." + }, + "step": { + "auth": { + "data": { + "host": "Host", + "security_code": "C\u00f3digo de seguridad" + }, + "description": "Puede encontrar el c\u00f3digo de seguridad en la parte posterior de su puerta de enlace.", + "title": "Ingrese el c\u00f3digo de seguridad" + } + }, + "title": "IKEA TR\u00c5DFRI" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/it.json b/homeassistant/components/tradfri/.translations/it.json index 3d5101bbce8..4c114492336 100644 --- a/homeassistant/components/tradfri/.translations/it.json +++ b/homeassistant/components/tradfri/.translations/it.json @@ -1,5 +1,23 @@ { "config": { + "abort": { + "already_configured": "Il bridge \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi al gateway.", + "invalid_key": "Impossibile registrarsi con la chiave fornita. Se questo continua a succedere, prova a riavviare il gateway.", + "timeout": "Tempo scaduto per la validazione del codice." + }, + "step": { + "auth": { + "data": { + "host": "Host", + "security_code": "Codice di sicurezza" + }, + "description": "Puoi trovare il codice di sicurezza sul retro del tuo gateway.", + "title": "Inserisci il codice di sicurezza" + } + }, "title": "IKEA TR\u00c5DFRI" } } \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/es-419.json b/homeassistant/components/twilio/.translations/es-419.json new file mode 100644 index 00000000000..a5fd83abef4 --- /dev/null +++ b/homeassistant/components/twilio/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir los mensajes de Twilio.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar [Webhooks with Twilio] ( {twilio_url} ). \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: ` {webhook_url} ` \n - M\u00e9todo: POST \n - Tipo de contenido: application / x-www-form-urlencoded \n\n Consulte [la documentaci\u00f3n] ( {docs_url} ) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1s seguro de que quieres configurar Twilio?", + "title": "Configurar el Webhook Twilio" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/hu.json b/homeassistant/components/twilio/.translations/hu.json index 257dd24f082..ae96d08976d 100644 --- a/homeassistant/components/twilio/.translations/hu.json +++ b/homeassistant/components/twilio/.translations/hu.json @@ -1,7 +1,15 @@ { "config": { "abort": { + "not_internet_accessible": "A Home Assistant rendszerednek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l a Twilio \u00fczenetek fogad\u00e1s\u00e1hoz.", "one_instance_allowed": "Csak egyetlen konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." - } + }, + "step": { + "user": { + "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Twilio-t?", + "title": "A Twilio Webhook be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "Twilio" } } \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/it.json b/homeassistant/components/twilio/.translations/it.json new file mode 100644 index 00000000000..4f8926c23e5 --- /dev/null +++ b/homeassistant/components/twilio/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Twilio.", + "one_instance_allowed": "\u00c8 necessaria una sola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, dovrai configurare [Webhooks con Twilio]({twilio_url})\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n - Content Type: application/x-www-form-urlencoded\n\n Vedi [la documentazione]({docs_url}) su come configurare le automazioni per gestire i dati in arrivo." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare Twilio?", + "title": "Configura il webhook di Twilio" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/es-419.json b/homeassistant/components/unifi/.translations/es-419.json new file mode 100644 index 00000000000..9b729e4c4ab --- /dev/null +++ b/homeassistant/components/unifi/.translations/es-419.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El sitio del controlador ya est\u00e1 configurado", + "user_privilege": "El usuario necesita ser administrador" + }, + "error": { + "faulty_credentials": "Credenciales de usuario incorrectas", + "service_unavailable": "No hay servicio disponible" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "site": "ID del sitio", + "username": "Nombre de usuario", + "verify_ssl": "Controlador usando el certificado apropiado" + }, + "title": "Configurar el controlador UniFi" + } + }, + "title": "Controlador UniFi" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/hu.json b/homeassistant/components/unifi/.translations/hu.json index 4a664a40c74..6f78beaffd6 100644 --- a/homeassistant/components/unifi/.translations/hu.json +++ b/homeassistant/components/unifi/.translations/hu.json @@ -14,9 +14,12 @@ "password": "Jelsz\u00f3", "port": "Port", "site": "Site azonos\u00edt\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "Vez\u00e9rl\u0151 megfelel\u0151 tan\u00fas\u00edtv\u00e1nnyal" + }, + "title": "UniFi vez\u00e9rl\u0151 be\u00e1ll\u00edt\u00e1sa" } - } + }, + "title": "UniFi Vez\u00e9rl\u0151" } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json new file mode 100644 index 00000000000..407371bf89f --- /dev/null +++ b/homeassistant/components/unifi/.translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Il sito del Controller \u00e8 gi\u00e0 configurato", + "user_privilege": "L'utente deve essere amministratore" + }, + "error": { + "faulty_credentials": "Credenziali utente non valide", + "service_unavailable": "Servizio non disponibile" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "site": "ID del sito", + "username": "Nome utente", + "verify_ssl": "Il Controller sta utilizzando il certificato corretto" + }, + "title": "Configura l'UniFi Controller" + } + }, + "title": "UniFi Controller" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/es-419.json b/homeassistant/components/upnp/.translations/es-419.json new file mode 100644 index 00000000000..bd95b48359e --- /dev/null +++ b/homeassistant/components/upnp/.translations/es-419.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD ya est\u00e1 configurado", + "incomplete_device": "Ignorar un dispositivo UPnP incompleto", + "no_devices_discovered": "No se han descubierto UPnP/IGDs", + "no_devices_found": "No se encuentran dispositivos UPnP/IGD en la red.", + "no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos", + "single_instance_allowed": "S\u00f3lo se necesita una \u00fanica configuraci\u00f3n de UPnP/IGD." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar UPnP/IGD?", + "title": "UPnP/IGD" + }, + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Habilitar la asignaci\u00f3n de puertos para Home Assistant", + "enable_sensors": "A\u00f1adir sensores de tr\u00e1fico", + "igd": "UPnP/IGD" + }, + "title": "Opciones de configuraci\u00f3n para UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/hu.json b/homeassistant/components/upnp/.translations/hu.json index f2fd380b1e3..7d3827e76da 100644 --- a/homeassistant/components/upnp/.translations/hu.json +++ b/homeassistant/components/upnp/.translations/hu.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "no_devices_found": "Nincsenek UPnPIGD eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton." + "already_configured": "Az UPnP / IGD m\u00e1r konfigur\u00e1l\u00e1sra ker\u00fclt", + "incomplete_device": "A hi\u00e1nyos UPnP-eszk\u00f6z figyelmen k\u00edv\u00fcl hagy\u00e1sa", + "no_devices_discovered": "Nem tal\u00e1ltam UPnP / IGD-ket", + "no_devices_found": "Nincsenek UPnPIGD eszk\u00f6z\u00f6k a h\u00e1l\u00f3zaton.", + "single_instance_allowed": "Csak egy UPnP / IGD konfigur\u00e1ci\u00f3 sz\u00fcks\u00e9ges." }, "error": { "one": "hiba", @@ -17,6 +21,7 @@ }, "user": { "data": { + "enable_port_mapping": "Enged\u00e9lyezd a port mappinget a Home Assistant sz\u00e1m\u00e1ra", "enable_sensors": "Forgalom \u00e9rz\u00e9kel\u0151k hozz\u00e1ad\u00e1sa", "igd": "UPnP/IGD" }, diff --git a/homeassistant/components/upnp/.translations/it.json b/homeassistant/components/upnp/.translations/it.json new file mode 100644 index 00000000000..798f6578093 --- /dev/null +++ b/homeassistant/components/upnp/.translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD \u00e8 gi\u00e0 configurato", + "incomplete_device": "Ignorare il dispositivo UPnP incompleto", + "no_devices_discovered": "Nessun UPnP/IGD trovato", + "no_devices_found": "Nessun dispositivo UPnP/IGD trovato in rete.", + "no_sensors_or_port_mapping": "Abilita almeno i sensori o la mappatura delle porte", + "single_instance_allowed": "\u00c8 necessaria una sola configurazione di UPnP/IGD." + }, + "step": { + "confirm": { + "description": "Vuoi configurare UPnP/IGD?", + "title": "UPnP/IGD" + }, + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Abilita il port mapping per Home Assistant", + "enable_sensors": "Aggiungi sensori di traffico", + "igd": "UPnP/IGD" + }, + "title": "Opzioni di configurazione per UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/ko.json b/homeassistant/components/upnp/.translations/ko.json index d38d5be58ba..9fa37e1236d 100644 --- a/homeassistant/components/upnp/.translations/ko.json +++ b/homeassistant/components/upnp/.translations/ko.json @@ -8,9 +8,6 @@ "no_sensors_or_port_mapping": "\ucd5c\uc18c\ud55c \uc13c\uc11c \ud639\uc740 \ud3ec\ud2b8 \ub9e4\ud551\uc744 \ud65c\uc131\ud654 \ud574\uc57c \ud569\ub2c8\ub2e4", "single_instance_allowed": "\ud558\ub098\uc758 UPnP/IGD \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, - "error": { - "other": "\ub2e4\ub978" - }, "step": { "confirm": { "description": "UPnP/IGD \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", diff --git a/homeassistant/components/zha/.translations/es-419.json b/homeassistant/components/zha/.translations/es-419.json new file mode 100644 index 00000000000..0047c762a9d --- /dev/null +++ b/homeassistant/components/zha/.translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de ZHA." + }, + "error": { + "cannot_connect": "No se puede conectar al dispositivo ZHA." + }, + "step": { + "user": { + "data": { + "radio_type": "Tipo de radio", + "usb_path": "Ruta del dispositivo USB" + }, + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/hu.json b/homeassistant/components/zha/.translations/hu.json index 11b2a9fc833..39c00a4dee3 100644 --- a/homeassistant/components/zha/.translations/hu.json +++ b/homeassistant/components/zha/.translations/hu.json @@ -12,6 +12,7 @@ "radio_type": "R\u00e1di\u00f3 t\u00edpusa", "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" }, + "description": "\u00dcres", "title": "ZHA" } }, diff --git a/homeassistant/components/zha/.translations/it.json b/homeassistant/components/zha/.translations/it.json new file mode 100644 index 00000000000..e4b87c9d7b6 --- /dev/null +++ b/homeassistant/components/zha/.translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di ZHA." + }, + "error": { + "cannot_connect": "Impossibile connettersi al dispositivo ZHA." + }, + "step": { + "user": { + "data": { + "radio_type": "Tipo di Radio", + "usb_path": "Percorso del dispositivo USB" + }, + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/es-419.json b/homeassistant/components/zwave/.translations/es-419.json new file mode 100644 index 00000000000..2e246fb9931 --- /dev/null +++ b/homeassistant/components/zwave/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave ya est\u00e1 configurado", + "one_instance_only": "El componente solo admite una instancia de Z-Wave" + }, + "step": { + "user": { + "data": { + "network_key": "Clave de red (dejar en blanco para auto-generar)", + "usb_path": "Ruta USB" + }, + "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n", + "title": "Configurar Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/hu.json b/homeassistant/components/zwave/.translations/hu.json index e326c5152a6..2842c535984 100644 --- a/homeassistant/components/zwave/.translations/hu.json +++ b/homeassistant/components/zwave/.translations/hu.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "A Z-Wave m\u00e1r konfigur\u00e1lva van" + "already_configured": "A Z-Wave m\u00e1r konfigur\u00e1lva van", + "one_instance_only": "Az \u00f6sszetev\u0151 csak egy Z-Wave p\u00e9ld\u00e1nyt t\u00e1mogat" + }, + "error": { + "option_error": "A Z-Wave \u00e9rv\u00e9nyes\u00edt\u00e9s sikertelen. Az USB-meghajt\u00f3 el\u00e9r\u00e9si \u00fatj\u00e1t helyesen adtad meg?" }, "step": { "user": { diff --git a/homeassistant/components/zwave/.translations/it.json b/homeassistant/components/zwave/.translations/it.json index 86a61307814..c380d8e5625 100644 --- a/homeassistant/components/zwave/.translations/it.json +++ b/homeassistant/components/zwave/.translations/it.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Z-Wave \u00e8 gi\u00e0 configurato", + "one_instance_only": "Il componente supporta solo un'istanza di Z-Wave" + }, + "error": { + "option_error": "Convalida Z-Wave fallita. Il percorso della chiavetta USB \u00e8 corretto?" + }, "step": { "user": { "data": { @@ -7,7 +14,7 @@ "usb_path": "Percorso USB" }, "description": "Vai su https://www.home-assistant.io/docs/z-wave/installation/ per le informazioni sulle variabili di configurazione", - "title": "Imposta Z-Wave" + "title": "Configura Z-Wave" } }, "title": "Z-Wave" From 3f29e913675687d20032d5df8a376fba06e63809 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 22 Feb 2019 14:11:07 +0100 Subject: [PATCH 112/253] Remove index (#21304) * Remove index * Remove emnumerate --- homeassistant/loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 3dfc996bb35..e36ad5451c1 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -160,8 +160,8 @@ def _load_file(hass, # type: HomeAssistant sys.path.insert(0, hass.config.config_dir) cache = hass.data[DATA_KEY] = {} - for index, path in enumerate('{}.{}'.format(base, comp_or_platform) - for base in base_paths): + for path in ('{}.{}'.format(base, comp_or_platform) + for base in base_paths): try: module = importlib.import_module(path) From 4102e244813145aea06b2c3cece120d095172b74 Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Fri, 22 Feb 2019 15:58:48 +0100 Subject: [PATCH 113/253] Adding myself as codeowner for Freebox component (#21288) --- CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index fde408b84a8..6fb00f382ea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -190,6 +190,9 @@ homeassistant/components/eight_sleep/* @mezz64 homeassistant/components/*/eight_sleep.py @mezz64 homeassistant/components/esphome/*.py @OttoWinter +# F +homeassistant/components/freebox/*.py @snoof85 + # G homeassistant/components/googlehome/* @ludeeus homeassistant/components/*/googlehome.py @ludeeus From caa3b123aee9acd269c9486c5c79b15f1f3ed74b Mon Sep 17 00:00:00 2001 From: Phil Cole Date: Fri, 22 Feb 2019 16:34:23 +0000 Subject: [PATCH 114/253] Nissanleaf (#21145) * Remove unneeded returns from handle_update() * Start __init__() params with hass. * Remove excess logging and downgrade remaining logging. * Remove period from end of comment * Decorate callback with @callback * Use more descriptive variables than key and value. * Inherit from BinarySensorDevice and overwrite is_on rather than state. * Removed uncheckedreturn values. * Use super() rather than explicit object. * Use add_entities instead of add_devices. * Don't use listener when calling immediately. * Remove some excess logging. * Switch to sync since pycarwings2 is sync. * Remove RuntimeError exception matching. * Add temporary reviewer comments. * Add UI help descriptions for update service. * Fix hound errors. * Replaced time.sleep() with await asyncio.sleep() * Removed location_updateon_on attribute since on device_tracker. * Use async_added_to_hass() and async_dispatcher_connect(). * Use dict[key] because schema key is required. * Clarify variable names. * Remove icon for charging switch. * Convert LeafChargeSwitch into service and sensor. * Use async_dispatcher_send(). * Add guard checks for discovery_info. Consistent logs. * Use async_schedul_update_ha_state(). * Device tracker should return true. * Remove icon for climate control. * Really remove icon for climate control. * Use register() instead of async_register(). * Add guard on device tracker if discovery_info is None. --- .../components/nissan_leaf/__init__.py | 210 ++++++++---------- .../components/nissan_leaf/binary_sensor.py | 44 +++- .../components/nissan_leaf/device_tracker.py | 24 +- .../components/nissan_leaf/sensor.py | 14 +- .../components/nissan_leaf/services.yaml | 21 ++ .../components/nissan_leaf/switch.py | 53 +---- 6 files changed, 176 insertions(+), 190 deletions(-) create mode 100644 homeassistant/components/nissan_leaf/services.yaml diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index f5a8217242d..cb101c0a530 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -1,13 +1,13 @@ """Support for the Nissan Leaf Carwings/Nissan Connect API.""" -import asyncio from datetime import datetime, timedelta +import asyncio import logging import sys -import urllib import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.dispatcher import ( @@ -78,32 +78,60 @@ LEAF_COMPONENTS = [ SIGNAL_UPDATE_LEAF = 'nissan_leaf_update' SERVICE_UPDATE_LEAF = 'update' +SERVICE_START_CHARGE_LEAF = 'start_charge' ATTR_VIN = 'vin' UPDATE_LEAF_SCHEMA = vol.Schema({ vol.Required(ATTR_VIN): cv.string, }) +START_CHARGE_LEAF_SCHEMA = vol.Schema({ + vol.Required(ATTR_VIN): cv.string, +}) -async def async_setup(hass, config): +def setup(hass, config): """Set up the Nissan Leaf component.""" import pycarwings2 - async def handle_update(service): + async def async_handle_update(service): + """Handle service to update leaf data from Nissan servers.""" # It would be better if this was changed to use nickname, or # an entity name rather than a vin. - vin = service.data.get(ATTR_VIN, '') + vin = service.data[ATTR_VIN] if vin in hass.data[DATA_LEAF]: data_store = hass.data[DATA_LEAF][vin] - async_track_point_in_utc_time( - hass, data_store.async_update_data, utcnow()) - return True + await data_store.async_update_data(utcnow()) + else: + _LOGGER.debug("Vin %s not recognised for update", vin) - _LOGGER.debug("Vin %s not recognised for update", vin) - return False + async def async_handle_start_charge(service): + """Handle service to start charging.""" + # It would be better if this was changed to use nickname, or + # an entity name rather than a vin. + vin = service.data[ATTR_VIN] - async def async_setup_leaf(car_config): + if vin in hass.data[DATA_LEAF]: + data_store = hass.data[DATA_LEAF][vin] + + # Send the command to request charging is started to Nissan + # servers. If that completes OK then trigger a fresh update to + # pull the charging status from the car after waiting a minute + # for the charging request to reach the car. + result = await hass.async_add_executor_job( + data_store.leaf.start_charging) + if result: + _LOGGER.debug("Start charging sent, " + "request updated data in 1 minute") + check_charge_at = utcnow() + timedelta(minutes=1) + data_store.next_update = check_charge_at + async_track_point_in_utc_time( + hass, data_store.async_update_data, check_charge_at) + + else: + _LOGGER.debug("Vin %s not recognised for update", vin) + + def setup_leaf(car_config): """Set up a car.""" _LOGGER.debug("Logging into You+Nissan...") @@ -112,20 +140,11 @@ async def async_setup(hass, config): region = car_config[CONF_REGION] leaf = None - async def leaf_login(): - nonlocal leaf - sess = pycarwings2.Session(username, password, region) - leaf = sess.get_leaf() - try: # This might need to be made async (somehow) causes # homeassistant to be slow to start - await hass.async_add_job(leaf_login) - except(RuntimeError, urllib.error.HTTPError): - _LOGGER.error( - "Unable to connect to Nissan Connect with " - "username and password") - return False + sess = pycarwings2.Session(username, password, region) + leaf = sess.get_leaf() except KeyError: _LOGGER.error( "Unable to fetch car details..." @@ -143,7 +162,7 @@ async def async_setup(hass, config): " as the drive train battery won't connect." " Don't set the intervals too low.") - data_store = LeafDataStore(leaf, hass, car_config) + data_store = LeafDataStore(hass, leaf, car_config) hass.data[DATA_LEAF][leaf.vin] = data_store for component in LEAF_COMPONENTS: @@ -154,12 +173,15 @@ async def async_setup(hass, config): utcnow() + INITIAL_UPDATE) hass.data[DATA_LEAF] = {} - tasks = [async_setup_leaf(car) for car in config[DOMAIN]] - if tasks: - await asyncio.wait(tasks, loop=hass.loop) + for car in config[DOMAIN]: + setup_leaf(car) - hass.services.async_register(DOMAIN, SERVICE_UPDATE_LEAF, handle_update, - schema=UPDATE_LEAF_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_UPDATE_LEAF, + async_handle_update, schema=UPDATE_LEAF_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_START_CHARGE_LEAF, + async_handle_start_charge, schema=START_CHARGE_LEAF_SCHEMA) return True @@ -167,13 +189,13 @@ async def async_setup(hass, config): class LeafDataStore: """Nissan Leaf Data Store.""" - def __init__(self, leaf, hass, car_config): + def __init__(self, hass, leaf, car_config): """Initialise the data store.""" + self.hass = hass self.leaf = leaf self.car_config = car_config self.nissan_connect = car_config[CONF_NCONNECT] self.force_miles = car_config[CONF_FORCE_MILES] - self.hass = hass self.data = {} self.data[DATA_CLIMATE] = False self.data[DATA_BATTERY] = 0 @@ -223,25 +245,19 @@ class LeafDataStore: if (self.last_battery_response is not None and self.data[DATA_CHARGING] is False and self.data[DATA_BATTERY] <= RESTRICTED_BATTERY): - _LOGGER.info("Low battery so restricting refresh frequency (%s)", - self.leaf.nickname) + _LOGGER.debug("Low battery so restricting refresh frequency (%s)", + self.leaf.nickname) interval = RESTRICTED_INTERVAL else: intervals = [base_interval] - _LOGGER.debug("Could use base interval=%s", base_interval) if self.data[DATA_CHARGING]: intervals.append(charging_interval) - _LOGGER.debug("Could use charging interval=%s", - charging_interval) if self.data[DATA_CLIMATE]: intervals.append(climate_interval) - _LOGGER.debug( - "Could use climate interval=%s", climate_interval) interval = min(intervals) - _LOGGER.debug("Resulting interval=%s", interval) return utcnow() + interval @@ -310,12 +326,10 @@ class LeafDataStore: _LOGGER.debug("Empty Location Response Received") self.data[DATA_LOCATION] = None else: - _LOGGER.debug("Got location data for Leaf") - self.data[DATA_LOCATION] = location_response - self.last_location_response = utcnow() - _LOGGER.debug("Location Response: %s", location_response.__dict__) + self.data[DATA_LOCATION] = location_response + self.last_location_response = utcnow() except CarwingsError: _LOGGER.error("Error fetching location info") @@ -336,9 +350,8 @@ class LeafDataStore: from pycarwings2 import CarwingsError try: # First, check nissan servers for the latest data - start_server_info = await self.hass.async_add_job( - self.leaf.get_latest_battery_status - ) + start_server_info = await self.hass.async_add_executor_job( + self.leaf.get_latest_battery_status) # Store the date from the nissan servers start_date = self._extract_start_date(start_server_info) @@ -346,33 +359,35 @@ class LeafDataStore: _LOGGER.info("No start date from servers. Aborting") return None - _LOGGER.info("Start server date=%s", start_date) + _LOGGER.debug("Start server date=%s", start_date) # Request battery update from the car - _LOGGER.info("Requesting battery update, %s", self.leaf.vin) - request = await self.hass.async_add_job(self.leaf.request_update) + _LOGGER.debug("Requesting battery update, %s", self.leaf.vin) + request = await self.hass.async_add_executor_job( + self.leaf.request_update) if not request: _LOGGER.error("Battery update request failed") return None for attempt in range(MAX_RESPONSE_ATTEMPTS): - _LOGGER.info("Waiting %s seconds for battery update (%s) (%s)", - PYCARWINGS2_SLEEP, self.leaf.vin, attempt) + _LOGGER.debug( + "Waiting %s seconds for battery update (%s) (%s)", + PYCARWINGS2_SLEEP, self.leaf.vin, attempt) await asyncio.sleep(PYCARWINGS2_SLEEP) # Note leaf.get_status_from_update is always returning 0, so # don't try to use it anymore. - server_info = await self.hass.async_add_job( - self.leaf.get_latest_battery_status - ) + server_info = await self.hass.async_add_executor_job( + self.leaf.get_latest_battery_status) latest_date = self._extract_start_date(server_info) - _LOGGER.info("Latest server date=%s", latest_date) + _LOGGER.debug("Latest server date=%s", latest_date) if latest_date is not None and latest_date != start_date: return server_info - _LOGGER.info("%s attempts exceeded return latest data from server", - MAX_RESPONSE_ATTEMPTS) + _LOGGER.debug( + "%s attempts exceeded return latest data from server", + MAX_RESPONSE_ATTEMPTS) return server_info except CarwingsError: _LOGGER.error("An error occurred getting battery status.") @@ -382,10 +397,8 @@ class LeafDataStore: """Request climate data from Nissan servers.""" from pycarwings2 import CarwingsError try: - request = await self.hass.async_add_job( - self.leaf.get_latest_hvac_status - ) - return request + return await self.hass.async_add_executor_job( + self.leaf.get_latest_hvac_status) except CarwingsError: _LOGGER.error( "An error occurred communicating with the car %s", @@ -396,43 +409,27 @@ class LeafDataStore: """Set climate control mode via Nissan servers.""" climate_result = None if toggle: - _LOGGER.info("Requesting climate turn on for %s", self.leaf.vin) - request = await self.hass.async_add_job( - self.leaf.start_climate_control - ) - for attempt in range(MAX_RESPONSE_ATTEMPTS): - if attempt > 0: - _LOGGER.info("Climate data not in yet (%s) (%s). " - "Waiting (%s) seconds.", self.leaf.vin, - attempt, PYCARWINGS2_SLEEP) - await asyncio.sleep(PYCARWINGS2_SLEEP) - - climate_result = await self.hass.async_add_job( - self.leaf.get_start_climate_control_result, request - ) - - if climate_result is not None: - break - + _LOGGER.debug("Requesting climate turn on for %s", self.leaf.vin) + set_function = self.leaf.start_climate_control + result_function = self.leaf.get_start_climate_control_result else: - _LOGGER.info("Requesting climate turn off for %s", self.leaf.vin) - request = await self.hass.async_add_job( - self.leaf.stop_climate_control - ) + _LOGGER.debug("Requesting climate turn off for %s", self.leaf.vin) + set_function = self.leaf.stop_climate_control + result_function = self.leaf.get_stop_climate_control_result - for attempt in range(MAX_RESPONSE_ATTEMPTS): - if attempt > 0: - _LOGGER.debug("Climate data not in yet. (%s) (%s). " - "Waiting %s seconds", self.leaf.vin, - attempt, PYCARWINGS2_SLEEP) - await asyncio.sleep(PYCARWINGS2_SLEEP) + request = await self.hass.async_add_executor_job(set_function) + for attempt in range(MAX_RESPONSE_ATTEMPTS): + if attempt > 0: + _LOGGER.debug("Climate data not in yet (%s) (%s). " + "Waiting (%s) seconds", self.leaf.vin, + attempt, PYCARWINGS2_SLEEP) + await asyncio.sleep(PYCARWINGS2_SLEEP) - climate_result = await self.hass.async_add_job( - self.leaf.get_stop_climate_control_result, request - ) + climate_result = await self.hass.async_add_executor_job( + result_function, request) - if climate_result is not None: - break + if climate_result is not None: + break if climate_result is not None: _LOGGER.debug("Climate result: %s", climate_result.__dict__) @@ -444,7 +441,8 @@ class LeafDataStore: async def async_get_location(self): """Get location from Nissan servers.""" - request = await self.hass.async_add_job(self.leaf.request_location) + request = await self.hass.async_add_executor_job( + self.leaf.request_location) for attempt in range(MAX_RESPONSE_ATTEMPTS): if attempt > 0: _LOGGER.debug("Location data not in yet. (%s) (%s). " @@ -452,9 +450,8 @@ class LeafDataStore: attempt, PYCARWINGS2_SLEEP) await asyncio.sleep(PYCARWINGS2_SLEEP) - location_status = await self.hass.async_add_job( - self.leaf.get_status_from_location, request - ) + location_status = await self.hass.async_add_executor_job( + self.leaf.get_status_from_location, request) if location_status is not None: _LOGGER.debug("Location_status=%s", location_status.__dict__) @@ -462,21 +459,6 @@ class LeafDataStore: return location_status - async def async_start_charging(self): - """Request start charging via Nissan servers.""" - # Send the command to request charging is started to Nissan servers. - # If that completes OK then trigger a fresh update to pull the - # charging status from the car after waiting a minute for the - # charging request to reach the car. - result = await self.hass.async_add_job(self.leaf.start_charging) - if result: - _LOGGER.debug("Start charging sent, " - "request updated data in 1 minute") - check_charge_at = utcnow() + timedelta(minutes=1) - self.next_update = check_charge_at - async_track_point_in_utc_time( - self.hass, self.async_update_data, check_charge_at) - class LeafEntity(Entity): """Base class for Nissan Leaf entity.""" @@ -499,7 +481,6 @@ class LeafEntity(Entity): 'last_attempt': self.car.last_check, 'updated_on': self.car.last_battery_response, 'update_in_progress': self.car.request_in_progress, - 'location_updated_on': self.car.last_location_response, 'vin': self.car.leaf.vin, } @@ -509,6 +490,7 @@ class LeafEntity(Entity): async_dispatcher_connect( self.car.hass, SIGNAL_UPDATE_LEAF, self._update_callback) + @callback def _update_callback(self): """Update the state.""" - self.schedule_update_ha_state(True) + self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/nissan_leaf/binary_sensor.py b/homeassistant/components/nissan_leaf/binary_sensor.py index 05255d616c4..2397405ec20 100644 --- a/homeassistant/components/nissan_leaf/binary_sensor.py +++ b/homeassistant/components/nissan_leaf/binary_sensor.py @@ -2,28 +2,29 @@ import logging from homeassistant.components.nissan_leaf import ( - DATA_LEAF, DATA_PLUGGED_IN, LeafEntity) + DATA_CHARGING, DATA_LEAF, DATA_PLUGGED_IN, LeafEntity) +from homeassistant.components.binary_sensor import BinarySensorDevice _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['nissan_leaf'] -def setup_platform(hass, config, add_devices, discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up of a Nissan Leaf binary sensor.""" - _LOGGER.debug( - "binary_sensor setup_platform, discovery_info=%s", discovery_info) + if discovery_info is None: + return devices = [] - for key, value in hass.data[DATA_LEAF].items(): - _LOGGER.debug( - "binary_sensor setup_platform, key=%s, value=%s", key, value) - devices.append(LeafPluggedInSensor(value)) + for vin, datastore in hass.data[DATA_LEAF].items(): + _LOGGER.debug("Adding binary_sensors for vin=%s", vin) + devices.append(LeafPluggedInSensor(datastore)) + devices.append(LeafChargingSensor(datastore)) - add_devices(devices, True) + add_entities(devices, True) -class LeafPluggedInSensor(LeafEntity): +class LeafPluggedInSensor(LeafEntity, BinarySensorDevice): """Plugged In Sensor class.""" @property @@ -32,7 +33,7 @@ class LeafPluggedInSensor(LeafEntity): return "{} {}".format(self.car.leaf.nickname, "Plug Status") @property - def state(self): + def is_on(self): """Return true if plugged in.""" return self.car.data[DATA_PLUGGED_IN] @@ -42,3 +43,24 @@ class LeafPluggedInSensor(LeafEntity): if self.car.data[DATA_PLUGGED_IN]: return 'mdi:power-plug' return 'mdi:power-plug-off' + + +class LeafChargingSensor(LeafEntity, BinarySensorDevice): + """Charging Sensor class.""" + + @property + def name(self): + """Sensor name.""" + return "{} {}".format(self.car.leaf.nickname, "Charging Status") + + @property + def is_on(self): + """Return true if charging.""" + return self.car.data[DATA_CHARGING] + + @property + def icon(self): + """Icon handling.""" + if self.car.data[DATA_CHARGING]: + return 'mdi:flash' + return 'mdi:flash-off' diff --git a/homeassistant/components/nissan_leaf/device_tracker.py b/homeassistant/components/nissan_leaf/device_tracker.py index 163675319ea..1ca7fceb911 100644 --- a/homeassistant/components/nissan_leaf/device_tracker.py +++ b/homeassistant/components/nissan_leaf/device_tracker.py @@ -15,28 +15,28 @@ ICON_CAR = "mdi:car" def setup_scanner(hass, config, see, discovery_info=None): """Set up the Nissan Leaf tracker.""" - _LOGGER.debug("Setting up Scanner (device_tracker) for Nissan Leaf, " - "discovery_info=%s", discovery_info) + if discovery_info is None: + return False def see_vehicle(): """Handle the reporting of the vehicle position.""" - for key, value in hass.data[DATA_LEAF].items(): - host_name = value.leaf.nickname + for vin, datastore in hass.data[DATA_LEAF].items(): + host_name = datastore.leaf.nickname dev_id = 'nissan_leaf_{}'.format(slugify(host_name)) - if not value.data[DATA_LOCATION]: - _LOGGER.debug("No position found for vehicle %s", key) - return False + if not datastore.data[DATA_LOCATION]: + _LOGGER.debug("No position found for vehicle %s", vin) + return _LOGGER.debug("Updating device_tracker for %s with position %s", - value.leaf.nickname, - value.data[DATA_LOCATION].__dict__) + datastore.leaf.nickname, + datastore.data[DATA_LOCATION].__dict__) attrs = { - 'updated_on': value.last_location_response, + 'updated_on': datastore.last_location_response, } see(dev_id=dev_id, host_name=host_name, gps=( - value.data[DATA_LOCATION].latitude, - value.data[DATA_LOCATION].longitude + datastore.data[DATA_LOCATION].latitude, + datastore.data[DATA_LOCATION].longitude ), attributes=attrs, icon=ICON_CAR) diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index 3c8f9ab9ef3..f6206f1f4ef 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -18,15 +18,15 @@ ICON_RANGE = 'mdi:speedometer' def setup_platform(hass, config, add_devices, discovery_info=None): """Sensors setup.""" - _LOGGER.debug("setup_platform nissan_leaf sensors, discovery_info=%s", - discovery_info) + if discovery_info is None: + return devices = [] - for key, value in hass.data[DATA_LEAF].items(): - _LOGGER.debug("adding sensor for item key=%s, value=%s", key, value) - devices.append(LeafBatterySensor(value)) - devices.append(LeafRangeSensor(value, True)) - devices.append(LeafRangeSensor(value, False)) + for vin, datastore in hass.data[DATA_LEAF].items(): + _LOGGER.debug("Adding sensors for vin=%s", vin) + devices.append(LeafBatterySensor(datastore)) + devices.append(LeafRangeSensor(datastore, True)) + devices.append(LeafRangeSensor(datastore, False)) add_devices(devices, True) diff --git a/homeassistant/components/nissan_leaf/services.yaml b/homeassistant/components/nissan_leaf/services.yaml new file mode 100644 index 00000000000..ef60dfb4a65 --- /dev/null +++ b/homeassistant/components/nissan_leaf/services.yaml @@ -0,0 +1,21 @@ +# Describes the format for available services for nissan_leaf + +start_charge: + description: > + Start the vehicle charging. It must be plugged in first! + fields: + vin: + description: > + The vehicle identification number (VIN) of the vehicle, 17 characters + example: WBANXXXXXX1234567 + +update: + description: > + Fetch the last state of the vehicle of all your accounts, requesting + an update from of the state from the car if possible. + fields: + vin: + description: > + The vehicle identification number (VIN) of the vehicle, 17 characters + example: WBANXXXXXX1234567 + diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py index 914e85b48a6..60b9a6630cd 100644 --- a/homeassistant/components/nissan_leaf/switch.py +++ b/homeassistant/components/nissan_leaf/switch.py @@ -2,7 +2,7 @@ import logging from homeassistant.components.nissan_leaf import ( - DATA_CHARGING, DATA_CLIMATE, DATA_LEAF, LeafEntity) + DATA_CLIMATE, DATA_LEAF, LeafEntity) from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) @@ -12,13 +12,13 @@ DEPENDENCIES = ['nissan_leaf'] def setup_platform(hass, config, add_devices, discovery_info=None): """Nissan Leaf switch platform setup.""" - _LOGGER.debug( - "In switch setup platform, discovery_info=%s", discovery_info) + if discovery_info is None: + return devices = [] - for value in hass.data[DATA_LEAF].values(): - devices.append(LeafChargeSwitch(value)) - devices.append(LeafClimateSwitch(value)) + for vin, datastore in hass.data[DATA_LEAF].items(): + _LOGGER.debug("Adding switch for vin=%s", vin) + devices.append(LeafClimateSwitch(datastore)) add_devices(devices, True) @@ -40,7 +40,7 @@ class LeafClimateSwitch(LeafEntity, ToggleEntity): @property def device_state_attributes(self): """Return climate control attributes.""" - attrs = super(LeafClimateSwitch, self).device_state_attributes + attrs = super().device_state_attributes attrs["updated_on"] = self.car.last_climate_response return attrs @@ -58,42 +58,3 @@ class LeafClimateSwitch(LeafEntity, ToggleEntity): """Turn off climate control.""" if await self.car.async_set_climate(False): self.car.data[DATA_CLIMATE] = False - - @property - def icon(self): - """Climate control icon.""" - if self.car.data[DATA_CLIMATE]: - return 'mdi:fan' - return 'mdi:fan-off' - - -class LeafChargeSwitch(LeafEntity, ToggleEntity): - """Nissan Leaf Charging On switch.""" - - @property - def name(self): - """Switch name.""" - return "{} {}".format(self.car.leaf.nickname, "Charging Status") - - @property - def icon(self): - """Charging switch icon.""" - if self.car.data[DATA_CHARGING]: - return 'mdi:flash' - return 'mdi:flash-off' - - @property - def is_on(self): - """Return true if charging.""" - return self.car.data[DATA_CHARGING] - - async def async_turn_on(self, **kwargs): - """Start car charging.""" - if await self.car.async_start_charging(): - self.car.data[DATA_CHARGING] = True - - def turn_off(self, **kwargs): - """Nissan API doesn't allow stopping of charge remotely.""" - _LOGGER.info( - "Cannot turn off Leaf charging." - " Nissan API does not support stopping charge remotely") From d9712027e8b7ce049a0313daead38e682ffb1bdb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 22 Feb 2019 17:59:43 +0100 Subject: [PATCH 115/253] Config entry options (#18929) Add support for options flow for config entries --- .../components/config/config_entries.py | 55 +++++- homeassistant/config_entries.py | 93 ++++++++- tests/common.py | 3 +- .../components/config/test_config_entries.py | 176 ++++++++++++++++-- tests/test_config_entries.py | 62 ++++++ 5 files changed, 365 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 68890a79ca6..65f65cbcec5 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -17,6 +17,10 @@ async def async_setup(hass): hass.http.register_view( ConfigManagerFlowResourceView(hass.config_entries.flow)) hass.http.register_view(ConfigManagerAvailableFlowView) + hass.http.register_view( + OptionManagerFlowIndexView(hass.config_entries.options.flow)) + hass.http.register_view( + OptionManagerFlowResourceView(hass.config_entries.options.flow)) return True @@ -45,8 +49,9 @@ class ConfigManagerEntryIndexView(HomeAssistantView): name = 'api:config:config_entries:entry' async def get(self, request): - """List flows in progress.""" + """List available config entries.""" hass = request.app['hass'] + return self.json([{ 'entry_id': entry.entry_id, 'domain': entry.domain, @@ -54,6 +59,9 @@ class ConfigManagerEntryIndexView(HomeAssistantView): 'source': entry.source, 'state': entry.state, 'connection_class': entry.connection_class, + 'supports_options': hasattr( + config_entries.HANDLERS[entry.domain], + 'async_get_options_flow'), } for entry in hass.config_entries.async_entries()]) @@ -145,3 +153,48 @@ class ConfigManagerAvailableFlowView(HomeAssistantView): async def get(self, request): """List available flow handlers.""" return self.json(config_entries.FLOWS) + + +class OptionManagerFlowIndexView(FlowManagerIndexView): + """View to create option flows.""" + + url = '/api/config/config_entries/entry/option/flow' + name = 'api:config:config_entries:entry:resource:option:flow' + + # pylint: disable=arguments-differ + async def post(self, request): + """Handle a POST request. + + handler in request is entry_id. + """ + if not request['hass_user'].is_admin: + raise Unauthorized( + perm_category=CAT_CONFIG_ENTRIES, permission='edit') + + # pylint: disable=no-value-for-parameter + return await super().post(request) + + +class OptionManagerFlowResourceView(ConfigManagerFlowResourceView): + """View to interact with the option flow manager.""" + + url = '/api/config/config_entries/options/flow/{flow_id}' + name = 'api:config:config_entries:options:flow:resource' + + async def get(self, request, flow_id): + """Get the current state of a data_entry_flow.""" + if not request['hass_user'].is_admin: + raise Unauthorized( + perm_category=CAT_CONFIG_ENTRIES, permission='edit') + + return await super().get(request, flow_id) + + # pylint: disable=arguments-differ + async def post(self, request, flow_id): + """Handle a POST request.""" + if not request['hass_user'].is_admin: + raise Unauthorized( + perm_category=CAT_CONFIG_ENTRIES, permission='edit') + + # pylint: disable=no-value-for-parameter + return await super().post(request, flow_id) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7ff93051c9d..34b30cf422a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -122,7 +122,8 @@ the flow from the config panel. import logging import functools import uuid -from typing import Set, Optional, List, Dict # noqa pylint: disable=unused-import +from typing import Callable, Dict, List, Optional, Set # noqa pylint: disable=unused-import +import weakref from homeassistant import data_entry_flow from homeassistant.core import callback, HomeAssistant @@ -130,7 +131,6 @@ from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry - _LOGGER = logging.getLogger(__name__) _UNDEF = object() @@ -223,12 +223,13 @@ CONN_CLASS_UNKNOWN = 'unknown' class ConfigEntry: """Hold a configuration entry.""" - __slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'source', - 'connection_class', 'state', '_setup_lock', - '_async_cancel_retry_setup') + __slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'options', + 'source', 'connection_class', 'state', '_setup_lock', + 'update_listeners', '_async_cancel_retry_setup') def __init__(self, version: str, domain: str, title: str, data: dict, source: str, connection_class: str, + options: Optional[dict] = None, entry_id: Optional[str] = None, state: str = ENTRY_STATE_NOT_LOADED) -> None: """Initialize a config entry.""" @@ -247,6 +248,9 @@ class ConfigEntry: # Config data self.data = data + # Entry options + self.options = options or {} + # Source of the configuration (user, discovery, cloud) self.source = source @@ -256,6 +260,9 @@ class ConfigEntry: # State of the entry (LOADED, NOT_LOADED) self.state = state + # Listeners to call on update + self.update_listeners = [] # type: list + # Function to cancel a scheduled retry self._async_cancel_retry_setup = None @@ -386,6 +393,18 @@ class ConfigEntry: self.title, component.DOMAIN) return False + def add_update_listener(self, listener: Callable) -> Callable: + """Listen for when entry is updated. + + Listener: Callback function(hass, entry) + + Returns function to unlisten. + """ + weak_listener = weakref.ref(listener) + self.update_listeners.append(weak_listener) + + return lambda: self.update_listeners.remove(weak_listener) + def as_dict(self): """Return dictionary version of this entry.""" return { @@ -394,6 +413,7 @@ class ConfigEntry: 'domain': self.domain, 'title': self.title, 'data': self.data, + 'options': self.options, 'source': self.source, 'connection_class': self.connection_class, } @@ -418,6 +438,7 @@ class ConfigEntries: self.hass = hass self.flow = data_entry_flow.FlowManager( hass, self._async_create_flow, self._async_finish_flow) + self.options = OptionsFlowManager(hass) self._hass_config = hass_config self._entries = [] # type: List[ConfigEntry] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @@ -435,6 +456,14 @@ class ConfigEntries: return result + @callback + def async_get_entry(self, entry_id: str) -> Optional[ConfigEntry]: + """Return entry with matching entry_id.""" + for entry in self._entries: + if entry_id == entry.entry_id: + return entry + return None + @callback def async_entries(self, domain: Optional[str] = None) -> List[ConfigEntry]: """Return all entries or entries for a specific domain.""" @@ -492,14 +521,25 @@ class ConfigEntries: title=entry['title'], # New in 0.79 connection_class=entry.get('connection_class', - CONN_CLASS_UNKNOWN)) + CONN_CLASS_UNKNOWN), + # New in 0.89 + options=entry.get('options')) for entry in config['entries']] @callback - def async_update_entry(self, entry, *, data=_UNDEF): + def async_update_entry(self, entry, *, data=_UNDEF, options=_UNDEF): """Update a config entry.""" if data is not _UNDEF: entry.data = data + + if options is not _UNDEF: + entry.options = options + + if data is not _UNDEF or options is not _UNDEF: + for listener_ref in entry.update_listeners: + listener = listener_ref() + self.hass.async_create_task(listener(self.hass, entry)) + self._async_schedule_save() async def async_forward_entry_setup(self, entry, component): @@ -549,6 +589,7 @@ class ConfigEntries: domain=result['handler'], title=result['title'], data=result['data'], + options={}, source=flow.context['source'], connection_class=flow.CONNECTION_CLASS, ) @@ -598,7 +639,7 @@ class ConfigEntries: flow.init_step = source return flow - def _async_schedule_save(self): + def _async_schedule_save(self) -> None: """Save the entity registry to a file.""" self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @@ -631,3 +672,39 @@ class ConfigFlow(data_entry_flow.FlowHandler): return [flw for flw in self.hass.config_entries.flow.async_progress() if flw['handler'] == self.handler and flw['flow_id'] != self.flow_id] + + +class OptionsFlowManager: + """Flow to set options for a configuration entry.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the options manager.""" + self.hass = hass + self.flow = data_entry_flow.FlowManager( + hass, self._async_create_flow, self._async_finish_flow) + + async def _async_create_flow(self, entry_id, *, context, data): + """Create an options flow for a config entry. + + Entry_id and flow.handler is the same thing to map entry with flow. + """ + entry = self.hass.config_entries.async_get_entry(entry_id) + if entry is None: + return + flow = HANDLERS[entry.domain].async_get_options_flow( + entry.data, entry.options) + return flow + + async def _async_finish_flow(self, flow, result): + """Finish an options flow and update options for configuration entry. + + Flow.handler and entry_id is the same thing to map flow with entry. + """ + entry = self.hass.config_entries.async_get_entry(flow.handler) + if entry is None: + return + self.hass.config_entries.async_update_entry( + entry, options=result['data']) + + result['result'] = True + return result diff --git a/tests/common.py b/tests/common.py index 0c1d6854886..a55546da73b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -609,13 +609,14 @@ class MockConfigEntry(config_entries.ConfigEntry): def __init__(self, *, domain='test', data=None, version=1, entry_id=None, source=config_entries.SOURCE_USER, title='Mock Title', - state=None, + state=None, options={}, connection_class=config_entries.CONN_CLASS_UNKNOWN): """Initialize a mock config entry.""" kwargs = { 'entry_id': entry_id or 'mock-id', 'domain': domain, 'data': data or {}, + 'options': options, 'version': version, 'title': title, 'connection_class': connection_class, diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index be73906c1bf..87ed83d9a7e 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -7,8 +7,9 @@ from unittest.mock import patch import pytest import voluptuous as vol -from homeassistant import config_entries as core_ce +from homeassistant import config_entries as core_ce, data_entry_flow from homeassistant.config_entries import HANDLERS +from homeassistant.core import callback from homeassistant.setup import async_setup_component from homeassistant.components.config import config_entries from homeassistant.loader import set_component @@ -30,25 +31,37 @@ def client(hass, hass_client): yield hass.loop.run_until_complete(hass_client()) -@asyncio.coroutine -def test_get_entries(hass, client): +async def test_get_entries(hass, client): """Test get entries.""" MockConfigEntry( - domain='comp', - title='Test 1', - source='bla', - connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + domain='comp', + title='Test 1', + source='bla', + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, ).add_to_hass(hass) MockConfigEntry( - domain='comp2', - title='Test 2', - source='bla2', - state=core_ce.ENTRY_STATE_LOADED, - connection_class=core_ce.CONN_CLASS_ASSUMED, + domain='comp2', + title='Test 2', + source='bla2', + state=core_ce.ENTRY_STATE_LOADED, + connection_class=core_ce.CONN_CLASS_ASSUMED, ).add_to_hass(hass) - resp = yield from client.get('/api/config/config_entries/entry') + + class CompConfigFlow: + @staticmethod + @callback + def async_get_options_flow(config, options): + pass + HANDLERS['comp'] = CompConfigFlow() + + class Comp2ConfigFlow: + def __init__(self): + pass + HANDLERS['comp2'] = Comp2ConfigFlow() + + resp = await client.get('/api/config/config_entries/entry') assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() for entry in data: entry.pop('entry_id') assert data == [ @@ -58,6 +71,7 @@ def test_get_entries(hass, client): 'source': 'bla', 'state': 'not_loaded', 'connection_class': 'local_poll', + 'supports_options': True, }, { 'domain': 'comp2', @@ -65,6 +79,7 @@ def test_get_entries(hass, client): 'source': 'bla2', 'state': 'loaded', 'connection_class': 'assumed', + 'supports_options': False, }, ] @@ -467,3 +482,136 @@ async def test_get_progress_flow_unauth(hass, client, hass_admin_user): '/api/config/config_entries/flow/{}'.format(data['flow_id'])) assert resp2.status == 401 + + +async def test_options_flow(hass, client): + """Test we can change options.""" + class TestFlow(core_ce.ConfigFlow): + @staticmethod + @callback + def async_get_options_flow(config, options): + class OptionsFlowHandler(data_entry_flow.FlowHandler): + def __init__(self, config, options): + self.config = config + self.options = options + + async def async_step_init(self, user_input=None): + schema = OrderedDict() + schema[vol.Required('enabled')] = bool + return self.async_show_form( + step_id='user', + data_schema=schema, + description_placeholders={ + 'enabled': 'Set to true to be true', + } + ) + return OptionsFlowHandler(config, options) + + MockConfigEntry( + domain='test', + entry_id='test1', + source='bla', + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + ).add_to_hass(hass) + entry = hass.config_entries._entries[0] + + with patch.dict(HANDLERS, {'test': TestFlow}): + url = '/api/config/config_entries/entry/option/flow' + resp = await client.post(url, json={'handler': entry.entry_id}) + + assert resp.status == 200 + data = await resp.json() + + data.pop('flow_id') + assert data == { + 'type': 'form', + 'handler': 'test1', + 'step_id': 'user', + 'data_schema': [ + { + 'name': 'enabled', + 'required': True, + 'type': 'boolean' + }, + ], + 'description_placeholders': { + 'enabled': 'Set to true to be true', + }, + 'errors': None + } + + +async def test_two_step_options_flow(hass, client): + """Test we can finish a two step options flow.""" + set_component( + hass, 'test', + MockModule('test', async_setup_entry=mock_coro_func(True))) + + class TestFlow(core_ce.ConfigFlow): + @staticmethod + @callback + def async_get_options_flow(config, options): + class OptionsFlowHandler(data_entry_flow.FlowHandler): + def __init__(self, config, options): + self.config = config + self.options = options + + async def async_step_init(self, user_input=None): + return self.async_show_form( + step_id='finish', + data_schema=vol.Schema({ + 'enabled': bool + }) + ) + + async def async_step_finish(self, user_input=None): + return self.async_create_entry( + title='Enable disable', + data=user_input + ) + return OptionsFlowHandler(config, options) + + MockConfigEntry( + domain='test', + entry_id='test1', + source='bla', + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + ).add_to_hass(hass) + entry = hass.config_entries._entries[0] + + with patch.dict(HANDLERS, {'test': TestFlow}): + url = '/api/config/config_entries/entry/option/flow' + resp = await client.post(url, json={'handler': entry.entry_id}) + + assert resp.status == 200 + data = await resp.json() + flow_id = data.pop('flow_id') + assert data == { + 'type': 'form', + 'handler': 'test1', + 'step_id': 'finish', + 'data_schema': [ + { + 'name': 'enabled', + 'type': 'boolean' + } + ], + 'description_placeholders': None, + 'errors': None + } + + with patch.dict(HANDLERS, {'test': TestFlow}): + resp = await client.post( + '/api/config/config_entries/options/flow/{}'.format(flow_id), + json={'enabled': True}) + assert resp.status == 200 + data = await resp.json() + data.pop('flow_id') + assert data == { + 'handler': 'test1', + 'type': 'create_entry', + 'title': 'Enable disable', + 'version': 1, + 'description': None, + 'description_placeholders': None, + } diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index e724680a05b..8991035cc22 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch import pytest from homeassistant import config_entries, loader, data_entry_flow +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -544,6 +545,31 @@ async def test_updating_entry_data(manager): } +async def test_update_entry_options_and_trigger_listener(hass, manager): + """Test that we can update entry options and trigger listener.""" + entry = MockConfigEntry( + domain='test', + options={'first': True}, + ) + entry.add_to_manager(manager) + + async def update_listener(hass, entry): + """Test function.""" + assert entry.options == { + 'second': True + } + + entry.add_update_listener(update_listener) + + manager.async_update_entry(entry, options={ + 'second': True + }) + + assert entry.options == { + 'second': True + } + + async def test_setup_raise_not_ready(hass, caplog): """Test a setup raising not ready.""" entry = MockConfigEntry(domain='test') @@ -588,3 +614,39 @@ async def test_setup_retrying_during_unload(hass): assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED assert len(mock_call.return_value.mock_calls) == 1 + + +async def test_entry_options(hass, manager): + """Test that we can set options on an entry.""" + entry = MockConfigEntry( + domain='test', + data={'first': True}, + options=None + ) + entry.add_to_manager(manager) + + class TestFlow: + @staticmethod + @callback + def async_get_options_flow(config, options): + class OptionsFlowHandler(data_entry_flow.FlowHandler): + def __init__(self, config, options): + pass + return OptionsFlowHandler(config, options) + + config_entries.HANDLERS['test'] = TestFlow() + flow = await manager.options._async_create_flow( + entry.entry_id, context={'source': 'test'}, data=None) + + flow.handler = entry.entry_id # Used to keep reference to config entry + + await manager.options._async_finish_flow( + flow, {'data': {'second': True}}) + + assert entry.data == { + 'first': True + } + + assert entry.options == { + 'second': True + } From 8b38b82e73d8ce83b3bd6200d2fd559d39c19e87 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 22 Feb 2019 13:35:12 -0600 Subject: [PATCH 116/253] Enhance SmartThings component subscription (#21124) * Move to config v2 to store SmartApp oauth keys * Add migration functionality. * Regenerate refresh token on periodic basis * Fix regenerate and misc. optimizations * Review feedback * Subscription sync logic now performs a difference operation * Removed config entry reloading. --- .../components/smartthings/__init__.py | 112 ++++++++++--- .../components/smartthings/config_flow.py | 50 +++--- homeassistant/components/smartthings/const.py | 7 + .../components/smartthings/smartapp.py | 126 +++++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/smartthings/conftest.py | 41 ++++- .../smartthings/test_binary_sensor.py | 34 ++-- .../smartthings/test_config_flow.py | 51 ++++-- tests/components/smartthings/test_fan.py | 45 ++---- tests/components/smartthings/test_init.py | 103 ++++++++++-- tests/components/smartthings/test_light.py | 49 ++---- tests/components/smartthings/test_smartapp.py | 149 +++++++++++++----- tests/components/smartthings/test_switch.py | 33 ++-- 14 files changed, 529 insertions(+), 275 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 3cf38c358bc..53ff6169c0a 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -14,16 +14,20 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .config_flow import SmartThingsFlowHandler # noqa from .const import ( - CONF_APP_ID, CONF_INSTALLED_APP_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN, - EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS) + CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_OAUTH_CLIENT_ID, + CONF_OAUTH_CLIENT_SECRET, CONF_REFRESH_TOKEN, DATA_BROKERS, DATA_MANAGER, + DOMAIN, EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS, + TOKEN_REFRESH_INTERVAL) from .smartapp import ( - setup_smartapp, setup_smartapp_endpoint, validate_installed_app) + setup_smartapp, setup_smartapp_endpoint, smartapp_sync_subscriptions, + validate_installed_app) -REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.2'] +REQUIREMENTS = ['pysmartapp==0.3.0', 'pysmartthings==0.6.3'] DEPENDENCIES = ['webhook'] _LOGGER = logging.getLogger(__name__) @@ -35,6 +39,33 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): return True +async def async_migrate_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Handle migration of a previous version config entry. + + A config entry created under a previous version must go through the + integration setup again so we can properly retrieve the needed data + elements. Force this by removing the entry and triggering a new flow. + """ + from pysmartthings import SmartThings + + # Delete the installed app + api = SmartThings(async_get_clientsession(hass), + entry.data[CONF_ACCESS_TOKEN]) + await api.delete_installed_app(entry.data[CONF_INSTALLED_APP_ID]) + # Delete the entry + hass.async_create_task( + hass.config_entries.async_remove(entry.entry_id)) + # only create new flow if there isn't a pending one for SmartThings. + flows = hass.config_entries.flow.async_progress() + if not [flow for flow in flows if flow['handler'] == DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={'source': 'import'})) + + # Return False because it could not be migrated. + return False + + async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Initialize config entry which represents an installed SmartApp.""" from pysmartthings import SmartThings @@ -62,6 +93,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): installed_app = await validate_installed_app( api, entry.data[CONF_INSTALLED_APP_ID]) + # Get SmartApp token to sync subscriptions + token = await api.generate_tokens( + entry.data[CONF_OAUTH_CLIENT_ID], + entry.data[CONF_OAUTH_CLIENT_SECRET], + entry.data[CONF_REFRESH_TOKEN]) + entry.data[CONF_REFRESH_TOKEN] = token.refresh_token + hass.config_entries.async_update_entry(entry) + # Get devices and their current status devices = await api.devices( location_ids=[installed_app.location_id]) @@ -71,18 +110,21 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): await device.status.refresh() except ClientResponseError: _LOGGER.debug("Unable to update status for device: %s (%s), " - "the device will be ignored", + "the device will be excluded", device.label, device.device_id, exc_info=True) devices.remove(device) await asyncio.gather(*[retrieve_device_status(d) for d in devices.copy()]) + # Sync device subscriptions + await smartapp_sync_subscriptions( + hass, token.access_token, installed_app.location_id, + installed_app.installed_app_id, devices) + # Setup device broker - broker = DeviceBroker(hass, devices, - installed_app.installed_app_id) - broker.event_handler_disconnect = \ - smart_app.connect_event(broker.event_handler) + broker = DeviceBroker(hass, entry, token, smart_app, devices) + broker.connect() hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker except ClientResponseError as ex: @@ -117,8 +159,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS].pop(entry.entry_id, None) - if broker and broker.event_handler_disconnect: - broker.event_handler_disconnect() + if broker: + broker.disconnect() tasks = [hass.config_entries.async_forward_entry_unload(entry, component) for component in SUPPORTED_PLATFORMS] @@ -128,14 +170,18 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): class DeviceBroker: """Manages an individual SmartThings config entry.""" - def __init__(self, hass: HomeAssistantType, devices: Iterable, - installed_app_id: str): + def __init__(self, hass: HomeAssistantType, entry: ConfigEntry, + token, smart_app, devices: Iterable): """Create a new instance of the DeviceBroker.""" self._hass = hass - self._installed_app_id = installed_app_id - self.assignments = self._assign_capabilities(devices) + self._entry = entry + self._installed_app_id = entry.data[CONF_INSTALLED_APP_ID] + self._smart_app = smart_app + self._token = token + self._event_disconnect = None + self._regenerate_token_remove = None + self._assignments = self._assign_capabilities(devices) self.devices = {device.device_id: device for device in devices} - self.event_handler_disconnect = None def _assign_capabilities(self, devices: Iterable): """Assign platforms to capabilities.""" @@ -158,17 +204,45 @@ class DeviceBroker: assignments[device.device_id] = slots return assignments + def connect(self): + """Connect handlers/listeners for device/lifecycle events.""" + # Setup interval to regenerate the refresh token on a periodic basis. + # Tokens expire in 30 days and once expired, cannot be recovered. + async def regenerate_refresh_token(now): + """Generate a new refresh token and update the config entry.""" + await self._token.refresh( + self._entry.data[CONF_OAUTH_CLIENT_ID], + self._entry.data[CONF_OAUTH_CLIENT_SECRET]) + self._entry.data[CONF_REFRESH_TOKEN] = self._token.refresh_token + self._hass.config_entries.async_update_entry(self._entry) + _LOGGER.debug('Regenerated refresh token for installed app: %s', + self._installed_app_id) + + self._regenerate_token_remove = async_track_time_interval( + self._hass, regenerate_refresh_token, TOKEN_REFRESH_INTERVAL) + + # Connect handler to incoming device events + self._event_disconnect = \ + self._smart_app.connect_event(self._event_handler) + + def disconnect(self): + """Disconnects handlers/listeners for device/lifecycle events.""" + if self._regenerate_token_remove: + self._regenerate_token_remove() + if self._event_disconnect: + self._event_disconnect() + def get_assigned(self, device_id: str, platform: str): """Get the capabilities assigned to the platform.""" - slots = self.assignments.get(device_id, {}) + slots = self._assignments.get(device_id, {}) return [key for key, value in slots.items() if value == platform] def any_assigned(self, device_id: str, platform: str): """Return True if the platform has any assigned capabilities.""" - slots = self.assignments.get(device_id, {}) + slots = self._assignments.get(device_id, {}) return any(value for value in slots.values() if value == platform) - async def event_handler(self, req, resp, app): + async def _event_handler(self, req, resp, app): """Broker for incoming events.""" from pysmartapp.event import EVENT_TYPE_DEVICE from pysmartthings import Capability, Attribute diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index 4663222c3b4..c290f0f8e55 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -9,7 +9,8 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( - CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, DOMAIN, + APP_OAUTH_CLIENT_NAME, APP_OAUTH_SCOPES, CONF_APP_ID, CONF_INSTALLED_APPS, + CONF_LOCATION_ID, CONF_OAUTH_CLIENT_ID, CONF_OAUTH_CLIENT_SECRET, DOMAIN, VAL_UID_MATCHER) from .smartapp import ( create_app, find_app, setup_smartapp, setup_smartapp_endpoint, update_app) @@ -35,7 +36,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): b) Config entries setup for all installations """ - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH def __init__(self): @@ -43,6 +44,8 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): self.access_token = None self.app_id = None self.api = None + self.oauth_client_secret = None + self.oauth_client_id = None async def async_step_import(self, user_input=None): """Occurs when a previously entry setup fails and is re-initiated.""" @@ -50,7 +53,7 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Get access token and validate it.""" - from pysmartthings import APIResponseError, SmartThings + from pysmartthings import APIResponseError, AppOAuth, SmartThings errors = {} if not self.hass.config.api.base_url.lower().startswith('https://'): @@ -83,10 +86,18 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): if app: await app.refresh() # load all attributes await update_app(self.hass, app) + # Get oauth client id/secret by regenerating it + app_oauth = AppOAuth(app.app_id) + app_oauth.client_name = APP_OAUTH_CLIENT_NAME + app_oauth.scope.extend(APP_OAUTH_SCOPES) + client = await self.api.generate_app_oauth(app_oauth) else: - app = await create_app(self.hass, self.api) + app, client = await create_app(self.hass, self.api) setup_smartapp(self.hass, app) self.app_id = app.app_id + self.oauth_client_secret = client.client_secret + self.oauth_client_id = client.client_id + except APIResponseError as ex: if ex.is_target_error(): errors['base'] = 'webhook_error' @@ -113,19 +124,23 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): async def async_step_wait_install(self, user_input=None): """Wait for SmartApp installation.""" - from pysmartthings import InstalledAppStatus - errors = {} if user_input is None: return self._show_step_wait_install(errors) # Find installed apps that were authorized - installed_apps = [app for app in await self.api.installed_apps( - installed_app_status=InstalledAppStatus.AUTHORIZED) - if app.app_id == self.app_id] + installed_apps = self.hass.data[DOMAIN][CONF_INSTALLED_APPS].copy() if not installed_apps: errors['base'] = 'app_not_installed' return self._show_step_wait_install(errors) + self.hass.data[DOMAIN][CONF_INSTALLED_APPS].clear() + + # Enrich the data + for installed_app in installed_apps: + installed_app[CONF_APP_ID] = self.app_id + installed_app[CONF_ACCESS_TOKEN] = self.access_token + installed_app[CONF_OAUTH_CLIENT_ID] = self.oauth_client_id + installed_app[CONF_OAUTH_CLIENT_SECRET] = self.oauth_client_secret # User may have installed the SmartApp in more than one SmartThings # location. Config flows are created for the additional installations @@ -133,21 +148,10 @@ class SmartThingsFlowHandler(config_entries.ConfigFlow): self.hass.async_create_task( self.hass.config_entries.flow.async_init( DOMAIN, context={'source': 'install'}, - data={ - CONF_APP_ID: installed_app.app_id, - CONF_INSTALLED_APP_ID: installed_app.installed_app_id, - CONF_LOCATION_ID: installed_app.location_id, - CONF_ACCESS_TOKEN: self.access_token - })) + data=installed_app)) - # return entity for the first one. - installed_app = installed_apps[0] - return await self.async_step_install({ - CONF_APP_ID: installed_app.app_id, - CONF_INSTALLED_APP_ID: installed_app.installed_app_id, - CONF_LOCATION_ID: installed_app.location_id, - CONF_ACCESS_TOKEN: self.access_token - }) + # Create config entity for the first one. + return await self.async_step_install(installed_apps[0]) def _show_step_user(self, errors): return self.async_show_form( diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index 27260b155d1..d423bcde44f 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -1,14 +1,20 @@ """Constants used by the SmartThings component and platforms.""" +from datetime import timedelta import re +APP_OAUTH_CLIENT_NAME = "Home Assistant" APP_OAUTH_SCOPES = [ 'r:devices:*' ] APP_NAME_PREFIX = 'homeassistant.' CONF_APP_ID = 'app_id' CONF_INSTALLED_APP_ID = 'installed_app_id' +CONF_INSTALLED_APPS = 'installed_apps' CONF_INSTANCE_ID = 'instance_id' CONF_LOCATION_ID = 'location_id' +CONF_OAUTH_CLIENT_ID = 'client_id' +CONF_OAUTH_CLIENT_SECRET = 'client_secret' +CONF_REFRESH_TOKEN = 'refresh_token' DATA_MANAGER = 'manager' DATA_BROKERS = 'brokers' DOMAIN = 'smartthings' @@ -29,6 +35,7 @@ SUPPORTED_PLATFORMS = [ 'binary_sensor', 'sensor' ] +TOKEN_REFRESH_INTERVAL = timedelta(days=14) VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]" \ "{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$" VAL_UID_MATCHER = re.compile(VAL_UID) diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 89043d4f76c..5527fda54f4 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -13,15 +13,16 @@ from uuid import uuid4 from aiohttp import web from homeassistant.components import webhook -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_WEBHOOK_ID +from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) from homeassistant.helpers.typing import HomeAssistantType from .const import ( - APP_NAME_PREFIX, APP_OAUTH_SCOPES, CONF_APP_ID, CONF_INSTALLED_APP_ID, - CONF_INSTANCE_ID, CONF_LOCATION_ID, DATA_BROKERS, DATA_MANAGER, DOMAIN, + APP_NAME_PREFIX, APP_OAUTH_CLIENT_NAME, APP_OAUTH_SCOPES, CONF_APP_ID, + CONF_INSTALLED_APP_ID, CONF_INSTALLED_APPS, CONF_INSTANCE_ID, + CONF_LOCATION_ID, CONF_REFRESH_TOKEN, DATA_BROKERS, DATA_MANAGER, DOMAIN, SETTINGS_INSTANCE_ID, SIGNAL_SMARTAPP_PREFIX, STORAGE_KEY, STORAGE_VERSION) _LOGGER = logging.getLogger(__name__) @@ -83,7 +84,7 @@ async def create_app(hass: HomeAssistantType, api): app = App() for key, value in template.items(): setattr(app, key, value) - app = (await api.create_app(app))[0] + app, client = await api.create_app(app) _LOGGER.debug("Created SmartApp '%s' (%s)", app.app_name, app.app_id) # Set unique hass id in settings @@ -97,12 +98,12 @@ async def create_app(hass: HomeAssistantType, api): # Set oauth scopes oauth = AppOAuth(app.app_id) - oauth.client_name = 'Home Assistant' + oauth.client_name = APP_OAUTH_CLIENT_NAME oauth.scope.extend(APP_OAUTH_SCOPES) await api.update_app_oauth(oauth) _LOGGER.debug("Updated App OAuth for SmartApp '%s' (%s)", app.app_name, app.app_id) - return app + return app, client async def update_app(hass: HomeAssistantType, app): @@ -185,32 +186,24 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): DATA_MANAGER: manager, CONF_INSTANCE_ID: config[CONF_INSTANCE_ID], DATA_BROKERS: {}, - CONF_WEBHOOK_ID: config[CONF_WEBHOOK_ID] + CONF_WEBHOOK_ID: config[CONF_WEBHOOK_ID], + CONF_INSTALLED_APPS: [] } async def smartapp_sync_subscriptions( hass: HomeAssistantType, auth_token: str, location_id: str, - installed_app_id: str, *, skip_delete=False): + installed_app_id: str, devices): """Synchronize subscriptions of an installed up.""" from pysmartthings import ( - CAPABILITIES, SmartThings, SourceType, Subscription) + CAPABILITIES, SmartThings, SourceType, Subscription, + SubscriptionEntity + ) api = SmartThings(async_get_clientsession(hass), auth_token) - devices = await api.devices(location_ids=[location_id]) + tasks = [] - # Build set of capabilities and prune unsupported ones - capabilities = set() - for device in devices: - capabilities.update(device.capabilities) - capabilities.intersection_update(CAPABILITIES) - - # Remove all (except for installs) - if not skip_delete: - await api.delete_subscriptions(installed_app_id) - - # Create for each capability - async def create_subscription(target): + async def create_subscription(target: str): sub = Subscription() sub.installed_app_id = installed_app_id sub.location_id = location_id @@ -224,52 +217,89 @@ async def smartapp_sync_subscriptions( _LOGGER.exception("Failed to create subscription for '%s' under " "app '%s'", target, installed_app_id) - tasks = [create_subscription(c) for c in capabilities] - await asyncio.gather(*tasks) + async def delete_subscription(sub: SubscriptionEntity): + try: + await api.delete_subscription( + installed_app_id, sub.subscription_id) + _LOGGER.debug("Removed subscription for '%s' under app '%s' " + "because it was no longer needed", + sub.capability, installed_app_id) + except Exception: # pylint:disable=broad-except + _LOGGER.exception("Failed to remove subscription for '%s' under " + "app '%s'", sub.capability, installed_app_id) + + # Build set of capabilities and prune unsupported ones + capabilities = set() + for device in devices: + capabilities.update(device.capabilities) + capabilities.intersection_update(CAPABILITIES) + + # Get current subscriptions and find differences + subscriptions = await api.subscriptions(installed_app_id) + for subscription in subscriptions: + if subscription.capability in capabilities: + capabilities.remove(subscription.capability) + else: + # Delete the subscription + tasks.append(delete_subscription(subscription)) + + # Remaining capabilities need subscriptions created + tasks.extend([create_subscription(c) for c in capabilities]) + + if tasks: + await asyncio.gather(*tasks) + else: + _LOGGER.debug("Subscriptions for app '%s' are up-to-date", + installed_app_id) async def smartapp_install(hass: HomeAssistantType, req, resp, app): """ Handle when a SmartApp is installed by the user into a location. - Setup subscriptions using the access token SmartThings provided in the - event. An explicit subscription is required for each 'capability' in order - to receive the related attribute updates. Finally, create a config entry - representing the installation if this is not the first installation under - the account. + Create a config entry representing the installation if this is not + the first installation under the account, otherwise store the data + for the config flow. """ - await smartapp_sync_subscriptions( - hass, req.auth_token, req.location_id, req.installed_app_id, - skip_delete=True) - - # The permanent access token is copied from another config flow with the - # same parent app_id. If one is not found, that means the user is within - # the initial config flow and the entry at the conclusion. - access_token = next(( - entry.data.get(CONF_ACCESS_TOKEN) for entry + install_data = { + CONF_INSTALLED_APP_ID: req.installed_app_id, + CONF_LOCATION_ID: req.location_id, + CONF_REFRESH_TOKEN: req.refresh_token + } + # App attributes (client id/secret, etc...) are copied from another entry + # with the same parent app_id. If one is not found, the install data is + # stored for the config flow to retrieve during the wait step. + entry = next(( + entry for entry in hass.config_entries.async_entries(DOMAIN) if entry.data[CONF_APP_ID] == app.app_id), None) - if access_token: + if entry: + data = entry.data.copy() + data.update(install_data) # Add as job not needed because the current coroutine was invoked # from the dispatcher and is not being awaited. await hass.config_entries.flow.async_init( DOMAIN, context={'source': 'install'}, - data={ - CONF_APP_ID: app.app_id, - CONF_INSTALLED_APP_ID: req.installed_app_id, - CONF_LOCATION_ID: req.location_id, - CONF_ACCESS_TOKEN: access_token - }) + data=data) + else: + # Store the data where the flow can find it + hass.data[DOMAIN][CONF_INSTALLED_APPS].append(install_data) async def smartapp_update(hass: HomeAssistantType, req, resp, app): """ Handle when a SmartApp is updated (reconfigured) by the user. - Synchronize subscriptions to ensure we're up-to-date. + Store the refresh token in the config entry. """ - await smartapp_sync_subscriptions( - hass, req.auth_token, req.location_id, req.installed_app_id) + # Update refresh token in config entry + entry = next((entry for entry in hass.config_entries.async_entries(DOMAIN) + if entry.data.get(CONF_INSTALLED_APP_ID) == + req.installed_app_id), + None) + if entry: + entry.data[CONF_REFRESH_TOKEN] = req.refresh_token + hass.config_entries.async_update_entry(entry) _LOGGER.debug("SmartApp '%s' under parent app '%s' was updated", req.installed_app_id, app.app_id) diff --git a/requirements_all.txt b/requirements_all.txt index d2bbaefce38..8f850c49af3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1252,7 +1252,7 @@ pysma==0.3.1 pysmartapp==0.3.0 # homeassistant.components.smartthings -pysmartthings==0.6.2 +pysmartthings==0.6.3 # homeassistant.components.device_tracker.snmp # homeassistant.components.sensor.snmp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f35d582bcab..64c28534046 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -223,7 +223,7 @@ pyqwikswitch==0.8 pysmartapp==0.3.0 # homeassistant.components.smartthings -pysmartthings==0.6.2 +pysmartthings==0.6.3 # homeassistant.components.sonos pysonos==0.0.6 diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index ee892fb03b9..4622e49b0c6 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -4,8 +4,8 @@ from unittest.mock import Mock, patch from uuid import uuid4 from pysmartthings import ( - CLASSIFICATION_AUTOMATION, AppEntity, AppSettings, DeviceEntity, - InstalledApp, Location) + CLASSIFICATION_AUTOMATION, AppEntity, AppOAuthClient, AppSettings, + DeviceEntity, InstalledApp, Location, Subscription) from pysmartthings.api import Api import pytest @@ -13,8 +13,9 @@ from homeassistant.components import webhook from homeassistant.components.smartthings import DeviceBroker from homeassistant.components.smartthings.const import ( APP_NAME_PREFIX, CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_INSTANCE_ID, - CONF_LOCATION_ID, DATA_BROKERS, DOMAIN, SETTINGS_INSTANCE_ID, STORAGE_KEY, - STORAGE_VERSION) + CONF_LOCATION_ID, CONF_OAUTH_CLIENT_ID, CONF_OAUTH_CLIENT_SECRET, + CONF_REFRESH_TOKEN, DATA_BROKERS, DOMAIN, SETTINGS_INSTANCE_ID, + STORAGE_KEY, STORAGE_VERSION) from homeassistant.config_entries import ( CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_WEBHOOK_ID @@ -26,9 +27,11 @@ from tests.common import mock_coro async def setup_platform(hass, platform: str, *devices): """Set up the SmartThings platform and prerequisites.""" hass.config.components.add(DOMAIN) - broker = DeviceBroker(hass, devices, '') - config_entry = ConfigEntry("1", DOMAIN, "Test", {}, + config_entry = ConfigEntry(2, DOMAIN, "Test", + {CONF_INSTALLED_APP_ID: str(uuid4())}, SOURCE_USER, CONN_CLASS_CLOUD_PUSH) + broker = DeviceBroker(hass, config_entry, Mock(), Mock(), devices) + hass.data[DOMAIN] = { DATA_BROKERS: { config_entry.entry_id: broker @@ -98,6 +101,15 @@ def app_fixture(hass, config_file): return app +@pytest.fixture(name="app_oauth_client") +def app_oauth_client_fixture(): + """Fixture for a single app's oauth.""" + return AppOAuthClient({ + 'oauthClientId': str(uuid4()), + 'oauthClientSecret': str(uuid4()) + }) + + @pytest.fixture(name='app_settings') def app_settings_fixture(app, config_file): """Fixture for an app settings.""" @@ -225,12 +237,25 @@ def config_entry_fixture(hass, installed_app, location): CONF_ACCESS_TOKEN: str(uuid4()), CONF_INSTALLED_APP_ID: installed_app.installed_app_id, CONF_APP_ID: installed_app.app_id, - CONF_LOCATION_ID: location.location_id + CONF_LOCATION_ID: location.location_id, + CONF_REFRESH_TOKEN: str(uuid4()), + CONF_OAUTH_CLIENT_ID: str(uuid4()), + CONF_OAUTH_CLIENT_SECRET: str(uuid4()) } - return ConfigEntry("1", DOMAIN, location.name, data, SOURCE_USER, + return ConfigEntry(2, DOMAIN, location.name, data, SOURCE_USER, CONN_CLASS_CLOUD_PUSH) +@pytest.fixture(name="subscription_factory") +def subscription_factory_fixture(): + """Fixture for creating mock subscriptions.""" + def _factory(capability): + sub = Subscription() + sub.capability = capability + return sub + return _factory + + @pytest.fixture(name="device_factory") def device_factory_fixture(): """Fixture for creating mock devices.""" diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 4b47537fa19..6e60ee49ca6 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -6,31 +6,15 @@ real HTTP calls are not initiated during testing. """ from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability -from homeassistant.components.binary_sensor import DEVICE_CLASSES -from homeassistant.components.smartthings import DeviceBroker, binary_sensor +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES, DOMAIN as BINARY_SENSOR_DOMAIN) +from homeassistant.components.smartthings import binary_sensor from homeassistant.components.smartthings.const import ( - DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) -from homeassistant.config_entries import ( - CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) + DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.helpers.dispatcher import async_dispatcher_send - -async def _setup_platform(hass, *devices): - """Set up the SmartThings binary_sensor platform and prerequisites.""" - hass.config.components.add(DOMAIN) - broker = DeviceBroker(hass, devices, '') - config_entry = ConfigEntry("1", DOMAIN, "Test", {}, - SOURCE_USER, CONN_CLASS_CLOUD_PUSH) - hass.data[DOMAIN] = { - DATA_BROKERS: { - config_entry.entry_id: broker - } - } - await hass.config_entries.async_forward_entry_setup( - config_entry, 'binary_sensor') - await hass.async_block_till_done() - return config_entry +from .conftest import setup_platform async def test_mapping_integrity(): @@ -56,7 +40,7 @@ async def test_entity_state(hass, device_factory): """Tests the state attributes properly match the light types.""" device = device_factory('Motion Sensor 1', [Capability.motion_sensor], {Attribute.motion: 'inactive'}) - await _setup_platform(hass, device) + await setup_platform(hass, BINARY_SENSOR_DOMAIN, device) state = hass.states.get('binary_sensor.motion_sensor_1_motion') assert state.state == 'off' assert state.attributes[ATTR_FRIENDLY_NAME] ==\ @@ -71,7 +55,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await _setup_platform(hass, device) + await setup_platform(hass, BINARY_SENSOR_DOMAIN, device) # Assert entry = entity_registry.async_get('binary_sensor.motion_sensor_1_motion') assert entry @@ -89,7 +73,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Motion Sensor 1', [Capability.motion_sensor], {Attribute.motion: 'inactive'}) - await _setup_platform(hass, device) + await setup_platform(hass, BINARY_SENSOR_DOMAIN, device) device.status.apply_attribute_update( 'main', Capability.motion_sensor, Attribute.motion, 'active') # Act @@ -107,7 +91,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Motion Sensor 1', [Capability.motion_sensor], {Attribute.motion: 'inactive'}) - config_entry = await _setup_platform(hass, device) + config_entry = await setup_platform(hass, BINARY_SENSOR_DOMAIN, device) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'binary_sensor') diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 7d335703131..28aa759a359 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -8,6 +8,9 @@ from pysmartthings import APIResponseError from homeassistant import data_entry_flow from homeassistant.components.smartthings.config_flow import ( SmartThingsFlowHandler) +from homeassistant.components.smartthings.const import ( + CONF_INSTALLED_APP_ID, CONF_INSTALLED_APPS, CONF_LOCATION_ID, + CONF_REFRESH_TOKEN, DOMAIN) from homeassistant.config_entries import ConfigEntry from tests.common import mock_coro @@ -171,14 +174,16 @@ async def test_unknown_error(hass, smartthings_mock): assert result['errors'] == {'base': 'app_setup_error'} -async def test_app_created_then_show_wait_form(hass, app, smartthings_mock): +async def test_app_created_then_show_wait_form( + hass, app, app_oauth_client, smartthings_mock): """Test SmartApp is created when one does not exist and shows wait form.""" flow = SmartThingsFlowHandler() flow.hass = hass smartthings = smartthings_mock.return_value smartthings.apps.return_value = mock_coro(return_value=[]) - smartthings.create_app.return_value = mock_coro(return_value=(app, None)) + smartthings.create_app.return_value = \ + mock_coro(return_value=(app, app_oauth_client)) smartthings.update_app_settings.return_value = mock_coro() smartthings.update_app_oauth.return_value = mock_coro() @@ -189,13 +194,15 @@ async def test_app_created_then_show_wait_form(hass, app, smartthings_mock): async def test_app_updated_then_show_wait_form( - hass, app, smartthings_mock): + hass, app, app_oauth_client, smartthings_mock): """Test SmartApp is updated when an existing is already created.""" flow = SmartThingsFlowHandler() flow.hass = hass api = smartthings_mock.return_value api.apps.return_value = mock_coro(return_value=[app]) + api.generate_app_oauth.return_value = \ + mock_coro(return_value=app_oauth_client) result = await flow.async_step_user({'access_token': str(uuid4())}) @@ -219,8 +226,6 @@ async def test_wait_form_displayed_after_checking(hass, smartthings_mock): flow = SmartThingsFlowHandler() flow.hass = hass flow.access_token = str(uuid4()) - flow.api = smartthings_mock.return_value - flow.api.installed_apps.return_value = mock_coro(return_value=[]) result = await flow.async_step_wait_install({}) @@ -235,19 +240,29 @@ async def test_config_entry_created_when_installed( flow = SmartThingsFlowHandler() flow.hass = hass flow.access_token = str(uuid4()) - flow.api = smartthings_mock.return_value flow.app_id = installed_app.app_id - flow.api.installed_apps.return_value = \ - mock_coro(return_value=[installed_app]) + flow.api = smartthings_mock.return_value + flow.oauth_client_id = str(uuid4()) + flow.oauth_client_secret = str(uuid4()) + data = { + CONF_REFRESH_TOKEN: str(uuid4()), + CONF_LOCATION_ID: installed_app.location_id, + CONF_INSTALLED_APP_ID: installed_app.installed_app_id + } + hass.data[DOMAIN][CONF_INSTALLED_APPS].append(data) result = await flow.async_step_wait_install({}) + assert not hass.data[DOMAIN][CONF_INSTALLED_APPS] assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['data']['app_id'] == installed_app.app_id assert result['data']['installed_app_id'] == \ installed_app.installed_app_id assert result['data']['location_id'] == installed_app.location_id assert result['data']['access_token'] == flow.access_token + assert result['data']['refresh_token'] == data[CONF_REFRESH_TOKEN] + assert result['data']['client_secret'] == flow.oauth_client_secret + assert result['data']['client_id'] == flow.oauth_client_id assert result['title'] == location.name @@ -259,17 +274,31 @@ async def test_multiple_config_entry_created_when_installed( flow.access_token = str(uuid4()) flow.app_id = app.app_id flow.api = smartthings_mock.return_value - flow.api.installed_apps.return_value = \ - mock_coro(return_value=installed_apps) + flow.oauth_client_id = str(uuid4()) + flow.oauth_client_secret = str(uuid4()) + for installed_app in installed_apps: + data = { + CONF_REFRESH_TOKEN: str(uuid4()), + CONF_LOCATION_ID: installed_app.location_id, + CONF_INSTALLED_APP_ID: installed_app.installed_app_id + } + hass.data[DOMAIN][CONF_INSTALLED_APPS].append(data) + install_data = hass.data[DOMAIN][CONF_INSTALLED_APPS].copy() result = await flow.async_step_wait_install({}) + assert not hass.data[DOMAIN][CONF_INSTALLED_APPS] + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['data']['app_id'] == installed_apps[0].app_id assert result['data']['installed_app_id'] == \ installed_apps[0].installed_app_id assert result['data']['location_id'] == installed_apps[0].location_id assert result['data']['access_token'] == flow.access_token + assert result['data']['refresh_token'] == \ + install_data[0][CONF_REFRESH_TOKEN] + assert result['data']['client_secret'] == flow.oauth_client_secret + assert result['data']['client_id'] == flow.oauth_client_id assert result['title'] == locations[0].name await hass.async_block_till_done() @@ -280,4 +309,6 @@ async def test_multiple_config_entry_created_when_installed( installed_apps[1].installed_app_id assert entries[0].data['location_id'] == installed_apps[1].location_id assert entries[0].data['access_token'] == flow.access_token + assert entries[0].data['client_secret'] == flow.oauth_client_secret + assert entries[0].data['client_id'] == flow.oauth_client_id assert entries[0].title == locations[1].name diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index db8d9b512de..644c0823fd5 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -7,31 +7,15 @@ real HTTP calls are not initiated during testing. from pysmartthings import Attribute, Capability from homeassistant.components.fan import ( - ATTR_SPEED, ATTR_SPEED_LIST, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, - SPEED_OFF, SUPPORT_SET_SPEED) -from homeassistant.components.smartthings import DeviceBroker, fan + ATTR_SPEED, ATTR_SPEED_LIST, DOMAIN as FAN_DOMAIN, SPEED_HIGH, SPEED_LOW, + SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED) +from homeassistant.components.smartthings import fan from homeassistant.components.smartthings.const import ( - DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) -from homeassistant.config_entries import ( - CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) + DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES from homeassistant.helpers.dispatcher import async_dispatcher_send - -async def _setup_platform(hass, *devices): - """Set up the SmartThings fan platform and prerequisites.""" - hass.config.components.add(DOMAIN) - broker = DeviceBroker(hass, devices, '') - config_entry = ConfigEntry("1", DOMAIN, "Test", {}, - SOURCE_USER, CONN_CLASS_CLOUD_PUSH) - hass.data[DOMAIN] = { - DATA_BROKERS: { - config_entry.entry_id: broker - } - } - await hass.config_entries.async_forward_entry_setup(config_entry, 'fan') - await hass.async_block_till_done() - return config_entry +from .conftest import setup_platform async def test_async_setup_platform(): @@ -45,7 +29,7 @@ async def test_entity_state(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'on', Attribute.fan_speed: 2}) - await _setup_platform(hass, device) + await setup_platform(hass, FAN_DOMAIN, device) # Dimmer 1 state = hass.states.get('fan.fan_1') @@ -63,11 +47,10 @@ async def test_entity_and_device_attributes(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'on', Attribute.fan_speed: 2}) - await _setup_platform(hass, device) + # Act + await setup_platform(hass, FAN_DOMAIN, device) entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() - # Act - await _setup_platform(hass, device) # Assert entry = entity_registry.async_get("fan.fan_1") assert entry @@ -88,7 +71,7 @@ async def test_turn_off(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'on', Attribute.fan_speed: 2}) - await _setup_platform(hass, device) + await setup_platform(hass, FAN_DOMAIN, device) # Act await hass.services.async_call( 'fan', 'turn_off', {'entity_id': 'fan.fan_1'}, @@ -106,7 +89,7 @@ async def test_turn_on(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await _setup_platform(hass, device) + await setup_platform(hass, FAN_DOMAIN, device) # Act await hass.services.async_call( 'fan', 'turn_on', {ATTR_ENTITY_ID: "fan.fan_1"}, @@ -124,7 +107,7 @@ async def test_turn_on_with_speed(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await _setup_platform(hass, device) + await setup_platform(hass, FAN_DOMAIN, device) # Act await hass.services.async_call( 'fan', 'turn_on', @@ -145,7 +128,7 @@ async def test_set_speed(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await _setup_platform(hass, device) + await setup_platform(hass, FAN_DOMAIN, device) # Act await hass.services.async_call( 'fan', 'set_speed', @@ -166,7 +149,7 @@ async def test_update_from_signal(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await _setup_platform(hass, device) + await setup_platform(hass, FAN_DOMAIN, device) await device.switch_on(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -185,7 +168,7 @@ async def test_unload_config_entry(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - config_entry = await _setup_platform(hass, device) + config_entry = await setup_platform(hass, FAN_DOMAIN, device) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'fan') diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 014cfe7da98..0e35ef80fc2 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -8,14 +8,33 @@ import pytest from homeassistant.components import smartthings from homeassistant.components.smartthings.const import ( - DATA_BROKERS, DOMAIN, EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, - SUPPORTED_PLATFORMS) + CONF_INSTALLED_APP_ID, CONF_REFRESH_TOKEN, DATA_BROKERS, DOMAIN, + EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from tests.common import mock_coro +async def test_migration_creates_new_flow( + hass, smartthings_mock, config_entry): + """Test migration deletes app and creates new flow.""" + config_entry.version = 1 + setattr(hass.config_entries, '_entries', [config_entry]) + api = smartthings_mock.return_value + api.delete_installed_app.return_value = mock_coro() + + await smartthings.async_migrate_entry(hass, config_entry) + + assert api.delete_installed_app.call_count == 1 + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(DOMAIN) + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]['handler'] == 'smartthings' + assert flows[0]['context'] == {'source': 'import'} + + async def test_unrecoverable_api_errors_create_new_flow( hass, config_entry, smartthings_mock): """ @@ -101,14 +120,22 @@ async def test_unauthorized_installed_app_raises_not_ready( async def test_config_entry_loads_platforms( hass, config_entry, app, installed_app, - device, smartthings_mock): + device, smartthings_mock, subscription_factory): """Test config entry loads properly and proxies to platforms.""" setattr(hass.config_entries, '_entries', [config_entry]) api = smartthings_mock.return_value api.app.return_value = mock_coro(return_value=app) api.installed_app.return_value = mock_coro(return_value=installed_app) - api.devices.return_value = mock_coro(return_value=[device]) + api.devices.side_effect = \ + lambda *args, **kwargs: mock_coro(return_value=[device]) + mock_token = Mock() + mock_token.access_token.return_value = str(uuid4()) + mock_token.refresh_token.return_value = str(uuid4()) + api.generate_tokens.return_value = mock_coro(return_value=mock_token) + subscriptions = [subscription_factory(capability) + for capability in device.capabilities] + api.subscriptions.return_value = mock_coro(return_value=subscriptions) with patch.object(hass.config_entries, 'async_forward_entry_setup', return_value=mock_coro()) as forward_mock: @@ -120,8 +147,12 @@ async def test_config_entry_loads_platforms( async def test_unload_entry(hass, config_entry): """Test entries are unloaded correctly.""" - broker = Mock() - broker.event_handler_disconnect = Mock() + connect_disconnect = Mock() + smart_app = Mock() + smart_app.connect_event.return_value = connect_disconnect + broker = smartthings.DeviceBroker( + hass, config_entry, Mock(), smart_app, []) + broker.connect() hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] = broker with patch.object(hass.config_entries, 'async_forward_entry_unload', @@ -129,15 +160,41 @@ async def test_unload_entry(hass, config_entry): return_value=True )) as forward_mock: assert await smartthings.async_unload_entry(hass, config_entry) - assert broker.event_handler_disconnect.call_count == 1 + + assert connect_disconnect.call_count == 1 assert config_entry.entry_id not in hass.data[DOMAIN][DATA_BROKERS] # Assert platforms unloaded await hass.async_block_till_done() assert forward_mock.call_count == len(SUPPORTED_PLATFORMS) +async def test_broker_regenerates_token( + hass, config_entry): + """Test the device broker regenerates the refresh token.""" + token = Mock() + token.refresh_token = str(uuid4()) + token.refresh.return_value = mock_coro() + stored_action = None + + def async_track_time_interval(hass, action, interval): + nonlocal stored_action + stored_action = action + + with patch('homeassistant.components.smartthings' + '.async_track_time_interval', + new=async_track_time_interval): + broker = smartthings.DeviceBroker( + hass, config_entry, token, Mock(), []) + broker.connect() + + assert stored_action + await stored_action(None) # pylint:disable=not-callable + assert token.refresh.call_count == 1 + assert config_entry.data[CONF_REFRESH_TOKEN] == token.refresh_token + + async def test_event_handler_dispatches_updated_devices( - hass, device_factory, event_request_factory): + hass, config_entry, device_factory, event_request_factory): """Test the event handler dispatches updated devices.""" devices = [ device_factory('Bedroom 1 Switch', ['switch']), @@ -147,6 +204,7 @@ async def test_event_handler_dispatches_updated_devices( device_ids = [devices[0].device_id, devices[1].device_id, devices[2].device_id] request = event_request_factory(device_ids) + config_entry.data[CONF_INSTALLED_APP_ID] = request.installed_app_id called = False def signal(ids): @@ -154,10 +212,13 @@ async def test_event_handler_dispatches_updated_devices( called = True assert device_ids == ids async_dispatcher_connect(hass, SIGNAL_SMARTTHINGS_UPDATE, signal) - broker = smartthings.DeviceBroker( - hass, devices, request.installed_app_id) - await broker.event_handler(request, None, None) + broker = smartthings.DeviceBroker( + hass, config_entry, Mock(), Mock(), devices) + broker.connect() + + # pylint:disable=protected-access + await broker._event_handler(request, None, None) await hass.async_block_till_done() assert called @@ -166,7 +227,7 @@ async def test_event_handler_dispatches_updated_devices( async def test_event_handler_ignores_other_installed_app( - hass, device_factory, event_request_factory): + hass, config_entry, device_factory, event_request_factory): """Test the event handler dispatches updated devices.""" device = device_factory('Bedroom 1 Switch', ['switch']) request = event_request_factory([device.device_id]) @@ -176,21 +237,26 @@ async def test_event_handler_ignores_other_installed_app( nonlocal called called = True async_dispatcher_connect(hass, SIGNAL_SMARTTHINGS_UPDATE, signal) - broker = smartthings.DeviceBroker(hass, [device], str(uuid4())) + broker = smartthings.DeviceBroker( + hass, config_entry, Mock(), Mock(), [device]) + broker.connect() - await broker.event_handler(request, None, None) + # pylint:disable=protected-access + await broker._event_handler(request, None, None) await hass.async_block_till_done() assert not called async def test_event_handler_fires_button_events( - hass, device_factory, event_factory, event_request_factory): + hass, config_entry, device_factory, event_factory, + event_request_factory): """Test the event handler fires button events.""" device = device_factory('Button 1', ['button']) event = event_factory(device.device_id, capability='button', attribute='button', value='pushed') request = event_request_factory(events=[event]) + config_entry.data[CONF_INSTALLED_APP_ID] = request.installed_app_id called = False def handler(evt): @@ -205,8 +271,11 @@ async def test_event_handler_fires_button_events( } hass.bus.async_listen(EVENT_BUTTON, handler) broker = smartthings.DeviceBroker( - hass, [device], request.installed_app_id) - await broker.event_handler(request, None, None) + hass, config_entry, Mock(), Mock(), [device]) + broker.connect() + + # pylint:disable=protected-access + await broker._event_handler(request, None, None) await hass.async_block_till_done() assert called diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index 72bc5da9063..d31507925d6 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -9,15 +9,16 @@ import pytest from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION) -from homeassistant.components.smartthings import DeviceBroker, light + DOMAIN as LIGHT_DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION) +from homeassistant.components.smartthings import light from homeassistant.components.smartthings.const import ( - DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) -from homeassistant.config_entries import ( - CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) + DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES from homeassistant.helpers.dispatcher import async_dispatcher_send +from .conftest import setup_platform + @pytest.fixture(name="light_devices") def light_devices_fixture(device_factory): @@ -44,22 +45,6 @@ def light_devices_fixture(device_factory): ] -async def _setup_platform(hass, *devices): - """Set up the SmartThings light platform and prerequisites.""" - hass.config.components.add(DOMAIN) - broker = DeviceBroker(hass, devices, '') - config_entry = ConfigEntry("1", DOMAIN, "Test", {}, - SOURCE_USER, CONN_CLASS_CLOUD_PUSH) - hass.data[DOMAIN] = { - DATA_BROKERS: { - config_entry.entry_id: broker - } - } - await hass.config_entries.async_forward_entry_setup(config_entry, 'light') - await hass.async_block_till_done() - return config_entry - - async def test_async_setup_platform(): """Test setup platform does nothing (it uses config entries).""" await light.async_setup_platform(None, None, None) @@ -67,7 +52,7 @@ async def test_async_setup_platform(): async def test_entity_state(hass, light_devices): """Tests the state attributes properly match the light types.""" - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, *light_devices) # Dimmer 1 state = hass.states.get('light.dimmer_1') @@ -101,7 +86,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await _setup_platform(hass, device) + await setup_platform(hass, LIGHT_DOMAIN, device) # Assert entry = entity_registry.async_get("light.light_1") assert entry @@ -118,7 +103,7 @@ async def test_entity_and_device_attributes(hass, device_factory): async def test_turn_off(hass, light_devices): """Test the light turns of successfully.""" # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, *light_devices) # Act await hass.services.async_call( 'light', 'turn_off', {'entity_id': 'light.color_dimmer_2'}, @@ -132,7 +117,7 @@ async def test_turn_off(hass, light_devices): async def test_turn_off_with_transition(hass, light_devices): """Test the light turns of successfully with transition.""" # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, *light_devices) # Act await hass.services.async_call( 'light', 'turn_off', @@ -147,7 +132,7 @@ async def test_turn_off_with_transition(hass, light_devices): async def test_turn_on(hass, light_devices): """Test the light turns of successfully.""" # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, *light_devices) # Act await hass.services.async_call( 'light', 'turn_on', {ATTR_ENTITY_ID: "light.color_dimmer_1"}, @@ -161,7 +146,7 @@ async def test_turn_on(hass, light_devices): async def test_turn_on_with_brightness(hass, light_devices): """Test the light turns on to the specified brightness.""" # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, *light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -185,7 +170,7 @@ async def test_turn_on_with_minimal_brightness(hass, light_devices): set the level to zero, which turns off the lights in SmartThings. """ # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, *light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -203,7 +188,7 @@ async def test_turn_on_with_minimal_brightness(hass, light_devices): async def test_turn_on_with_color(hass, light_devices): """Test the light turns on with color.""" # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, *light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -220,7 +205,7 @@ async def test_turn_on_with_color(hass, light_devices): async def test_turn_on_with_color_temp(hass, light_devices): """Test the light turns on with color temp.""" # Arrange - await _setup_platform(hass, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, *light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -244,7 +229,7 @@ async def test_update_from_signal(hass, device_factory): status={Attribute.switch: 'off', Attribute.level: 100, Attribute.hue: 76.0, Attribute.saturation: 55.0, Attribute.color_temperature: 4500}) - await _setup_platform(hass, device) + await setup_platform(hass, LIGHT_DOMAIN, device) await device.switch_on(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -266,7 +251,7 @@ async def test_unload_config_entry(hass, device_factory): status={Attribute.switch: 'off', Attribute.level: 100, Attribute.hue: 76.0, Attribute.saturation: 55.0, Attribute.color_temperature: 4500}) - config_entry = await _setup_platform(hass, device) + config_entry = await setup_platform(hass, LIGHT_DOMAIN, device) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'light') diff --git a/tests/components/smartthings/test_smartapp.py b/tests/components/smartthings/test_smartapp.py index 162a8f9a4e5..46bd1f42f7f 100644 --- a/tests/components/smartthings/test_smartapp.py +++ b/tests/components/smartthings/test_smartapp.py @@ -5,7 +5,9 @@ from uuid import uuid4 from pysmartthings import AppEntity, Capability from homeassistant.components.smartthings import smartapp -from homeassistant.components.smartthings.const import DATA_MANAGER, DOMAIN +from homeassistant.components.smartthings.const import ( + CONF_INSTALLED_APP_ID, CONF_INSTALLED_APPS, CONF_LOCATION_ID, + CONF_REFRESH_TOKEN, DATA_MANAGER, DOMAIN) from tests.common import mock_coro @@ -35,31 +37,26 @@ async def test_update_app_updated_needed(hass, app): assert mock_app.classifications == app.classifications -async def test_smartapp_install_abort_if_no_other( +async def test_smartapp_install_store_if_no_other( hass, smartthings_mock, device_factory): """Test aborts if no other app was configured already.""" # Arrange - api = smartthings_mock.return_value - api.create_subscription.return_value = mock_coro() app = Mock() app.app_id = uuid4() request = Mock() - request.installed_app_id = uuid4() - request.auth_token = uuid4() - request.location_id = uuid4() - devices = [ - device_factory('', [Capability.battery, 'ping']), - device_factory('', [Capability.switch, Capability.switch_level]), - device_factory('', [Capability.switch]) - ] - api.devices = Mock() - api.devices.return_value = mock_coro(return_value=devices) + request.installed_app_id = str(uuid4()) + request.auth_token = str(uuid4()) + request.location_id = str(uuid4()) + request.refresh_token = str(uuid4()) # Act await smartapp.smartapp_install(hass, request, None, app) # Assert entries = hass.config_entries.async_entries('smartthings') assert not entries - assert api.create_subscription.call_count == 3 + data = hass.data[DOMAIN][CONF_INSTALLED_APPS][0] + assert data[CONF_REFRESH_TOKEN] == request.refresh_token + assert data[CONF_LOCATION_ID] == request.location_id + assert data[CONF_INSTALLED_APP_ID] == request.installed_app_id async def test_smartapp_install_creates_flow( @@ -68,12 +65,12 @@ async def test_smartapp_install_creates_flow( # Arrange setattr(hass.config_entries, '_entries', [config_entry]) api = smartthings_mock.return_value - api.create_subscription.return_value = mock_coro() app = Mock() app.app_id = config_entry.data['app_id'] request = Mock() request.installed_app_id = str(uuid4()) request.auth_token = str(uuid4()) + request.refresh_token = str(uuid4()) request.location_id = location.location_id devices = [ device_factory('', [Capability.battery, 'ping']), @@ -88,42 +85,42 @@ async def test_smartapp_install_creates_flow( await hass.async_block_till_done() entries = hass.config_entries.async_entries('smartthings') assert len(entries) == 2 - assert api.create_subscription.call_count == 3 assert entries[1].data['app_id'] == app.app_id assert entries[1].data['installed_app_id'] == request.installed_app_id assert entries[1].data['location_id'] == request.location_id assert entries[1].data['access_token'] == \ config_entry.data['access_token'] + assert entries[1].data['refresh_token'] == request.refresh_token + assert entries[1].data['client_secret'] == \ + config_entry.data['client_secret'] + assert entries[1].data['client_id'] == config_entry.data['client_id'] assert entries[1].title == location.name -async def test_smartapp_update_syncs_subs( - hass, smartthings_mock, config_entry, location, device_factory): - """Test update synchronizes subscriptions.""" +async def test_smartapp_update_saves_token( + hass, smartthings_mock, location, device_factory): + """Test update saves token.""" # Arrange - setattr(hass.config_entries, '_entries', [config_entry]) + entry = Mock() + entry.data = { + 'installed_app_id': str(uuid4()), + 'app_id': str(uuid4()) + } + entry.domain = DOMAIN + + setattr(hass.config_entries, '_entries', [entry]) app = Mock() - app.app_id = config_entry.data['app_id'] - api = smartthings_mock.return_value - api.delete_subscriptions = Mock() - api.delete_subscriptions.return_value = mock_coro() - api.create_subscription.return_value = mock_coro() + app.app_id = entry.data['app_id'] request = Mock() - request.installed_app_id = str(uuid4()) + request.installed_app_id = entry.data['installed_app_id'] request.auth_token = str(uuid4()) + request.refresh_token = str(uuid4()) request.location_id = location.location_id - devices = [ - device_factory('', [Capability.battery, 'ping']), - device_factory('', [Capability.switch, Capability.switch_level]), - device_factory('', [Capability.switch]) - ] - api.devices = Mock() - api.devices.return_value = mock_coro(return_value=devices) + # Act await smartapp.smartapp_update(hass, request, None, app) # Assert - assert api.create_subscription.call_count == 3 - assert api.delete_subscriptions.call_count == 1 + assert entry.data[CONF_REFRESH_TOKEN] == request.refresh_token async def test_smartapp_uninstall(hass, config_entry): @@ -152,3 +149,83 @@ async def test_smartapp_webhook(hass): result = await smartapp.smartapp_webhook(hass, '', request) assert result.body == b'{}' + + +async def test_smartapp_sync_subscriptions( + hass, smartthings_mock, device_factory, subscription_factory): + """Test synchronization adds and removes.""" + api = smartthings_mock.return_value + api.delete_subscription.side_effect = lambda loc_id, sub_id: mock_coro() + api.create_subscription.side_effect = lambda sub: mock_coro() + subscriptions = [ + subscription_factory(Capability.thermostat), + subscription_factory(Capability.switch), + subscription_factory(Capability.switch_level) + ] + api.subscriptions.return_value = mock_coro(return_value=subscriptions) + devices = [ + device_factory('', [Capability.battery, 'ping']), + device_factory('', [Capability.switch, Capability.switch_level]), + device_factory('', [Capability.switch]) + ] + + await smartapp.smartapp_sync_subscriptions( + hass, str(uuid4()), str(uuid4()), str(uuid4()), devices) + + assert api.subscriptions.call_count == 1 + assert api.delete_subscription.call_count == 1 + assert api.create_subscription.call_count == 1 + + +async def test_smartapp_sync_subscriptions_up_to_date( + hass, smartthings_mock, device_factory, subscription_factory): + """Test synchronization does nothing when current.""" + api = smartthings_mock.return_value + api.delete_subscription.side_effect = lambda loc_id, sub_id: mock_coro() + api.create_subscription.side_effect = lambda sub: mock_coro() + subscriptions = [ + subscription_factory(Capability.battery), + subscription_factory(Capability.switch), + subscription_factory(Capability.switch_level) + ] + api.subscriptions.return_value = mock_coro(return_value=subscriptions) + devices = [ + device_factory('', [Capability.battery, 'ping']), + device_factory('', [Capability.switch, Capability.switch_level]), + device_factory('', [Capability.switch]) + ] + + await smartapp.smartapp_sync_subscriptions( + hass, str(uuid4()), str(uuid4()), str(uuid4()), devices) + + assert api.subscriptions.call_count == 1 + assert api.delete_subscription.call_count == 0 + assert api.create_subscription.call_count == 0 + + +async def test_smartapp_sync_subscriptions_handles_exceptions( + hass, smartthings_mock, device_factory, subscription_factory): + """Test synchronization does nothing when current.""" + api = smartthings_mock.return_value + api.delete_subscription.side_effect = \ + lambda loc_id, sub_id: mock_coro(exception=Exception) + api.create_subscription.side_effect = \ + lambda sub: mock_coro(exception=Exception) + subscriptions = [ + subscription_factory(Capability.battery), + subscription_factory(Capability.switch), + subscription_factory(Capability.switch_level) + ] + api.subscriptions.return_value = mock_coro(return_value=subscriptions) + devices = [ + device_factory('', [Capability.thermostat, 'ping']), + device_factory('', [Capability.switch, Capability.switch_level]), + device_factory('', [Capability.switch]) + ] + + await smartapp.smartapp_sync_subscriptions( + hass, str(uuid4()), str(uuid4()), str(uuid4()), devices) + + assert api.subscriptions.call_count == 1 + assert api.delete_subscription.call_count == 1 + assert api.create_subscription.call_count == 1 diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 3f2bedd4f13..6ad87b7ad53 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -6,28 +6,13 @@ real HTTP calls are not initiated during testing. """ from pysmartthings import Attribute, Capability -from homeassistant.components.smartthings import DeviceBroker, switch +from homeassistant.components.smartthings import switch from homeassistant.components.smartthings.const import ( - DATA_BROKERS, DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) -from homeassistant.config_entries import ( - CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry) + DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.helpers.dispatcher import async_dispatcher_send - -async def _setup_platform(hass, *devices): - """Set up the SmartThings switch platform and prerequisites.""" - hass.config.components.add(DOMAIN) - broker = DeviceBroker(hass, devices, '') - config_entry = ConfigEntry("1", DOMAIN, "Test", {}, - SOURCE_USER, CONN_CLASS_CLOUD_PUSH) - hass.data[DOMAIN] = { - DATA_BROKERS: { - config_entry.entry_id: broker - } - } - await hass.config_entries.async_forward_entry_setup(config_entry, 'switch') - await hass.async_block_till_done() - return config_entry +from .conftest import setup_platform async def test_async_setup_platform(): @@ -43,7 +28,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await _setup_platform(hass, device) + await setup_platform(hass, SWITCH_DOMAIN, device) # Assert entry = entity_registry.async_get('switch.switch_1') assert entry @@ -62,7 +47,7 @@ async def test_turn_off(hass, device_factory): # Arrange device = device_factory('Switch_1', [Capability.switch], {Attribute.switch: 'on'}) - await _setup_platform(hass, device) + await setup_platform(hass, SWITCH_DOMAIN, device) # Act await hass.services.async_call( 'switch', 'turn_off', {'entity_id': 'switch.switch_1'}, @@ -78,7 +63,7 @@ async def test_turn_on(hass, device_factory): # Arrange device = device_factory('Switch_1', [Capability.switch], {Attribute.switch: 'off'}) - await _setup_platform(hass, device) + await setup_platform(hass, SWITCH_DOMAIN, device) # Act await hass.services.async_call( 'switch', 'turn_on', {'entity_id': 'switch.switch_1'}, @@ -94,7 +79,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Switch_1', [Capability.switch], {Attribute.switch: 'off'}) - await _setup_platform(hass, device) + await setup_platform(hass, SWITCH_DOMAIN, device) await device.switch_on(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -111,7 +96,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Switch 1', [Capability.switch], {Attribute.switch: 'on'}) - config_entry = await _setup_platform(hass, device) + config_entry = await setup_platform(hass, SWITCH_DOMAIN, device) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'switch') From 2b3c31cdb0104c5e2d50d7445028ca84790e0271 Mon Sep 17 00:00:00 2001 From: philipperequile Date: Fri, 22 Feb 2019 23:52:46 +0100 Subject: [PATCH 117/253] Add more ads plc types (#19801) * Update __init__.py Support for 2 new 4-byte PLC datatypes DINT and UDINT * Update __init__.py * Fix lint issue * Update __init__.py * Update __init__.py * Update __init__.py * Support for 2 new PLC types Sensor supports DINT and UDINT PLC types * Update __init__.py I removed unused TYPES = [ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE, ADSTYPE_DINT, ADSTYPE_UDINT] --- homeassistant/components/ads/__init__.py | 37 ++++++++++++++++-------- homeassistant/components/ads/sensor.py | 3 +- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index cfd0f37caa0..060e9b2b987 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -4,9 +4,11 @@ import struct import logging import ctypes from collections import namedtuple + import voluptuous as vol -from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \ - EVENT_HOMEASSISTANT_STOP + +from homeassistant.const import ( + CONF_DEVICE, CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pyads==3.0.7'] @@ -16,18 +18,20 @@ _LOGGER = logging.getLogger(__name__) DATA_ADS = 'data_ads' # Supported Types -ADSTYPE_INT = 'int' -ADSTYPE_UINT = 'uint' -ADSTYPE_BYTE = 'byte' ADSTYPE_BOOL = 'bool' +ADSTYPE_BYTE = 'byte' +ADSTYPE_DINT = 'dint' +ADSTYPE_INT = 'int' +ADSTYPE_UDINT = 'udint' +ADSTYPE_UINT = 'uint' -DOMAIN = 'ads' - +CONF_ADS_FACTOR = 'factor' +CONF_ADS_TYPE = 'adstype' +CONF_ADS_VALUE = 'value' CONF_ADS_VAR = 'adsvar' CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness' -CONF_ADS_TYPE = 'adstype' -CONF_ADS_FACTOR = 'factor' -CONF_ADS_VALUE = 'value' + +DOMAIN = 'ads' SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name' @@ -41,7 +45,8 @@ CONFIG_SCHEMA = vol.Schema({ SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({ vol.Required(CONF_ADS_TYPE): - vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE, ADSTYPE_BOOL]), + vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE, ADSTYPE_BOOL, + ADSTYPE_DINT, ADSTYPE_UDINT]), vol.Required(CONF_ADS_VALUE): vol.Coerce(int), vol.Required(CONF_ADS_VAR): cv.string, }) @@ -61,15 +66,19 @@ def setup(hass, config): AdsHub.ADS_TYPEMAP = { ADSTYPE_BOOL: pyads.PLCTYPE_BOOL, ADSTYPE_BYTE: pyads.PLCTYPE_BYTE, + ADSTYPE_DINT: pyads.PLCTYPE_DINT, ADSTYPE_INT: pyads.PLCTYPE_INT, + ADSTYPE_UDINT: pyads.PLCTYPE_UDINT, ADSTYPE_UINT: pyads.PLCTYPE_UINT, } + AdsHub.ADSError = pyads.ADSError AdsHub.PLCTYPE_BOOL = pyads.PLCTYPE_BOOL AdsHub.PLCTYPE_BYTE = pyads.PLCTYPE_BYTE + AdsHub.PLCTYPE_DINT = pyads.PLCTYPE_DINT AdsHub.PLCTYPE_INT = pyads.PLCTYPE_INT + AdsHub.PLCTYPE_UDINT = pyads.PLCTYPE_UDINT AdsHub.PLCTYPE_UINT = pyads.PLCTYPE_UINT - AdsHub.ADSError = pyads.ADSError try: ads = AdsHub(client) @@ -192,6 +201,10 @@ class AdsHub: value = struct.unpack(' Date: Fri, 22 Feb 2019 18:13:40 -0500 Subject: [PATCH 118/253] Allow custom_effect to be absent from Flux configuration (#21317) * Allow custom_effect to be absent from Flux configuration * set custom effect to none during setup --- homeassistant/components/light/flux_led.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 5ecf3f55e10..bfbb98ad57e 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -151,6 +151,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device['name'] = '{} {}'.format(device['id'], ipaddr) device[ATTR_MODE] = MODE_RGBW device[CONF_PROTOCOL] = None + device[CONF_CUSTOM_EFFECT] = None light = FluxLight(device) lights.append(light) From 111f882d78a8abc0f31c272408aef7954ad6b758 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 22 Feb 2019 17:39:19 -0800 Subject: [PATCH 119/253] Add note to issue template regarding iOS issues (#21342) ## Description: Add an additional line to the issue template mentioning that iOS issues should be reported to the home-assistant-iOS repo. --- .github/ISSUE_TEMPLATE.md | 1 + .github/ISSUE_TEMPLATE/Bug_report.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index e58659c876a..57244b44d9a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -2,6 +2,7 @@ - If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/ - Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases - Frontend issues should be submitted to the home-assistant-polymer repository: https://github.com/home-assistant/home-assistant-polymer/issues +- iOS issues should be submitted to the home-assistant-iOS repository: https://github.com/home-assistant/home-assistant-iOS/issues - Do not report issues for components if you are using custom components: files in /custom_components - This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests - Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template! diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index d9ab141d61c..2abfa6f9b6f 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -8,6 +8,7 @@ about: Create a report to help us improve - If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/ - Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases - Frontend issues should be submitted to the home-assistant-polymer repository: https://github.com/home-assistant/home-assistant-polymer/issues +- iOS issues should be submitted to the home-assistant-iOS repository: https://github.com/home-assistant/home-assistant-iOS/issues - Do not report issues for components if you are using custom components: files in /custom_components - This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests - Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template! From 2aa7bdb1d5259d70976fe93cb035e08e418eb953 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Sat, 23 Feb 2019 01:01:09 -0800 Subject: [PATCH 120/253] Allow google home component device tracker to be optional (#21335) --- homeassistant/components/googlehome/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/googlehome/__init__.py b/homeassistant/components/googlehome/__init__.py index d6a8be58967..6ebc2f512b1 100644 --- a/homeassistant/components/googlehome/__init__.py +++ b/homeassistant/components/googlehome/__init__.py @@ -20,6 +20,7 @@ NAME = 'GoogleHome' CONF_DEVICE_TYPES = 'device_types' CONF_RSSI_THRESHOLD = 'rssi_threshold' CONF_TRACK_ALARMS = 'track_alarms' +CONF_TRACK_DEVICES = 'track_devices' DEVICE_TYPES = [1, 2, 3] DEFAULT_RSSI_THRESHOLD = -70 @@ -31,6 +32,7 @@ DEVICE_CONFIG = vol.Schema({ vol.Optional(CONF_RSSI_THRESHOLD, default=DEFAULT_RSSI_THRESHOLD): vol.Coerce(int), vol.Optional(CONF_TRACK_ALARMS, default=False): cv.boolean, + vol.Optional(CONF_TRACK_DEVICES, default=True): cv.boolean, }) @@ -48,9 +50,10 @@ async def async_setup(hass, config): for device in config[DOMAIN][CONF_DEVICES]: hass.data[DOMAIN][device['host']] = {} - hass.async_create_task( - discovery.async_load_platform( - hass, 'device_tracker', DOMAIN, device, config)) + if device[CONF_TRACK_DEVICES]: + hass.async_create_task( + discovery.async_load_platform( + hass, 'device_tracker', DOMAIN, device, config)) if device[CONF_TRACK_ALARMS]: hass.async_create_task( From 8f70c168639aba2db682b0478eb9a4028c862a57 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sat, 23 Feb 2019 10:13:15 +0100 Subject: [PATCH 121/253] Add LCN cover platform (#20288) * Add LCN cover platform * Removed unused default value * Moved cover component to lcn platform directory. Small changes due to change request * Closed state is set before updating --- homeassistant/components/lcn/__init__.py | 38 ++++++---- homeassistant/components/lcn/cover.py | 89 ++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 12 deletions(-) create mode 100755 homeassistant/components/lcn/cover.py diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 941160b6397..6fb9f58a1d6 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -5,8 +5,8 @@ import re import voluptuous as vol from homeassistant.const import ( - CONF_ADDRESS, CONF_HOST, CONF_LIGHTS, CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_SWITCHES, CONF_USERNAME) + CONF_ADDRESS, CONF_COVERS, CONF_HOST, CONF_LIGHTS, CONF_NAME, + CONF_PASSWORD, CONF_PORT, CONF_SWITCHES, CONF_USERNAME) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity @@ -25,11 +25,15 @@ CONF_OUTPUT = 'output' CONF_TRANSITION = 'transition' CONF_DIMMABLE = 'dimmable' CONF_CONNECTIONS = 'connections' +CONF_MOTOR = 'motor' DIM_MODES = ['STEPS50', 'STEPS200'] OUTPUT_PORTS = ['OUTPUT1', 'OUTPUT2', 'OUTPUT3', 'OUTPUT4'] RELAY_PORTS = ['RELAY1', 'RELAY2', 'RELAY3', 'RELAY4', - 'RELAY5', 'RELAY6', 'RELAY7', 'RELAY8'] + 'RELAY5', 'RELAY6', 'RELAY7', 'RELAY8', + 'MOTORONOFF1', 'MOTORUPDOWN1', 'MOTORONOFF2', 'MOTORUPDOWN2', + 'MOTORONOFF3', 'MOTORUPDOWN3', 'MOTORONOFF4', 'MOTORUPDOWN4'] +MOTOR_PORTS = ['MOTOR1', 'MOTOR2', 'MOTOR3', 'MOTOR4'] # Regex for address validation PATTERN_ADDRESS = re.compile('^((?P\\w+)\\.)?s?(?P\\d+)' @@ -78,6 +82,12 @@ def is_address(value): raise vol.error.Invalid('Not a valid address string.') +COVERS_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ADDRESS): is_address, + vol.Required(CONF_MOTOR): vol.All(vol.Upper, vol.In(MOTOR_PORTS)) + }) + LIGHTS_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): is_address, @@ -111,8 +121,12 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_CONNECTIONS): vol.All( cv.ensure_list, has_unique_connection_names, [CONNECTION_SCHEMA]), - vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA]), - vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCHES_SCHEMA]) + vol.Optional(CONF_COVERS): vol.All( + cv.ensure_list, [COVERS_SCHEMA]), + vol.Optional(CONF_LIGHTS): vol.All( + cv.ensure_list, [LIGHTS_SCHEMA]), + vol.Optional(CONF_SWITCHES): vol.All( + cv.ensure_list, [SWITCHES_SCHEMA]) }) }, extra=vol.ALLOW_EXTRA) @@ -166,13 +180,13 @@ async def async_setup(hass, config): hass.data[DATA_LCN][CONF_CONNECTIONS] = connections - hass.async_create_task( - async_load_platform(hass, 'light', DOMAIN, - config[DOMAIN][CONF_LIGHTS], config)) - - hass.async_create_task( - async_load_platform(hass, 'switch', DOMAIN, - config[DOMAIN][CONF_SWITCHES], config)) + # load platforms + for component, conf_key in (('cover', CONF_COVERS), + ('light', CONF_LIGHTS), + ('switch', CONF_SWITCHES)): + hass.async_create_task( + async_load_platform(hass, component, DOMAIN, + config[DOMAIN][conf_key], config)) return True diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py new file mode 100755 index 00000000000..8151144f077 --- /dev/null +++ b/homeassistant/components/lcn/cover.py @@ -0,0 +1,89 @@ +"""Support for LCN covers.""" +from homeassistant.components.cover import CoverDevice +from homeassistant.components.lcn import ( + CONF_CONNECTIONS, CONF_MOTOR, DATA_LCN, LcnDevice, get_connection) +from homeassistant.const import CONF_ADDRESS + +DEPENDENCIES = ['lcn'] + + +async def async_setup_platform(hass, hass_config, async_add_entities, + discovery_info=None): + """Setups the LCN cover platform.""" + if discovery_info is None: + return + + import pypck + + devices = [] + for config in discovery_info: + address, connection_id = config[CONF_ADDRESS] + addr = pypck.lcn_addr.LcnAddr(*address) + connections = hass.data[DATA_LCN][CONF_CONNECTIONS] + connection = get_connection(connections, connection_id) + address_connection = connection.get_address_conn(addr) + + devices.append(LcnCover(config, address_connection)) + + async_add_entities(devices) + + +class LcnCover(LcnDevice, CoverDevice): + """Representation of a LCN cover.""" + + def __init__(self, config, address_connection): + """Initialize the LCN cover.""" + super().__init__(config, address_connection) + + self.motor = self.pypck.lcn_defs.MotorPort[config[CONF_MOTOR]] + self.motor_port_onoff = self.motor.value * 2 + self.motor_port_updown = self.motor_port_onoff + 1 + + self._closed = None + + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self.hass.async_create_task( + self.address_connection.activate_status_request_handler( + self.motor)) + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self._closed + + async def async_close_cover(self, **kwargs): + """Close the cover.""" + self._closed = True + states = [self.pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 + states[self.motor.value] = self.pypck.lcn_defs.MotorStateModifier.DOWN + self.address_connection.control_motors(states) + await self.async_update_ha_state() + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + self._closed = False + states = [self.pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 + states[self.motor.value] = self.pypck.lcn_defs.MotorStateModifier.UP + self.address_connection.control_motors(states) + await self.async_update_ha_state() + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + self._closed = None + states = [self.pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 + states[self.motor.value] = self.pypck.lcn_defs.MotorStateModifier.STOP + self.address_connection.control_motors(states) + await self.async_update_ha_state() + + def input_received(self, input_obj): + """Set cover states when LCN input object (command) is received.""" + if not isinstance(input_obj, self.pypck.inputs.ModStatusRelays): + return + + states = input_obj.states # list of boolean values (relay on/off) + if states[self.motor_port_onoff]: # motor is on + self._closed = states[self.motor_port_updown] # set direction + + self.async_schedule_update_ha_state() From b7b4a6dcc84a29da6f0fea0274729c0eb5a3e7e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 23 Feb 2019 10:35:18 +0100 Subject: [PATCH 122/253] Add location to Norway airquality (#21347) --- homeassistant/components/air_quality/norway_air.py | 6 ++++-- homeassistant/components/weather/met.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/air_quality/norway_air.py b/homeassistant/components/air_quality/norway_air.py index 372f3ec079d..712f2734ea8 100644 --- a/homeassistant/components/air_quality/norway_air.py +++ b/homeassistant/components/air_quality/norway_air.py @@ -17,7 +17,7 @@ from homeassistant.const import (CONF_LATITUDE, CONF_LONGITUDE, from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['pyMetno==0.4.5'] +REQUIREMENTS = ['pyMetno==0.4.6'] _LOGGER = logging.getLogger(__name__) @@ -91,7 +91,9 @@ class AirSensor(AirQualityEntity): @property def device_state_attributes(self) -> dict: """Return other details about the sensor state.""" - return {'level': self._api.data.get('level')} + return {'level': self._api.data.get('level'), + 'location': self._api.data.get('location'), + } @property def name(self) -> str: diff --git a/homeassistant/components/weather/met.py b/homeassistant/components/weather/met.py index 3ddbe5aed75..6c9613ac5d2 100644 --- a/homeassistant/components/weather/met.py +++ b/homeassistant/components/weather/met.py @@ -13,7 +13,7 @@ from homeassistant.helpers.event import ( async_call_later, async_track_utc_time_change) import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pyMetno==0.4.5'] +REQUIREMENTS = ['pyMetno==0.4.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8f850c49af3..e8d4d15d928 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -897,7 +897,7 @@ pyHS100==0.3.4 # homeassistant.components.air_quality.norway_air # homeassistant.components.weather.met -pyMetno==0.4.5 +pyMetno==0.4.6 # homeassistant.components.rfxtrx pyRFXtrx==0.23 From 48e44f4b5b7a87bec93f4ddccb8678dd1016a4ed Mon Sep 17 00:00:00 2001 From: Julius Mittenzwei Date: Sat, 23 Feb 2019 14:04:32 +0100 Subject: [PATCH 123/253] Updated pyvlx from 0.2.8 to 0.2.9. This version has slightly improved logging (#21349) --- homeassistant/components/velux/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 1018f72fdbc..6ea50ae6c0d 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -12,7 +12,7 @@ DATA_VELUX = "data_velux" SUPPORTED_DOMAINS = ['cover', 'scene'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pyvlx==0.2.8'] +REQUIREMENTS = ['pyvlx==0.2.9'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/requirements_all.txt b/requirements_all.txt index e8d4d15d928..2dc3d8c5be8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1438,7 +1438,7 @@ pyvesync==0.1.1 pyvizio==0.0.4 # homeassistant.components.velux -pyvlx==0.2.8 +pyvlx==0.2.9 # homeassistant.components.notify.html5 pywebpush==1.6.0 From 954bd4e13b4925047ea4f490a27882bfafb10efb Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Sat, 23 Feb 2019 14:43:37 +0100 Subject: [PATCH 124/253] Update buienradar.py (#21351) Fixing warning about unsupported config key in Buienradar sensor, whilst it was actually supported (but not validated) --- homeassistant/components/sensor/buienradar.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 36585b8e103..4ceb1b221c0 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -137,6 +137,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'Latitude and longitude must exist together'): cv.longitude, vol.Optional(CONF_TIMEFRAME, default=60): vol.All(vol.Coerce(int), vol.Range(min=5, max=120)), + vol.Optional(CONF_NAME, default='br'): cv.string, }) @@ -161,7 +162,7 @@ async def async_setup_platform(hass, config, async_add_entities, dev = [] for sensor_type in config[CONF_MONITORED_CONDITIONS]: - dev.append(BrSensor(sensor_type, config.get(CONF_NAME, 'br'), + dev.append(BrSensor(sensor_type, config.get(CONF_NAME), coordinates)) async_add_entities(dev) From 616c7628d7ce23e96ae22bdad31e0356b234cbab Mon Sep 17 00:00:00 2001 From: siberx Date: Sat, 23 Feb 2019 05:51:34 -0800 Subject: [PATCH 125/253] Fixes the sensor.filter outlier filter (handle step-changes correctly) (#21332) * Fix outlier filter median return, Add/update filter outlier tests * Switch outlier filter to store raw vals (handles step-changes correctly) * Filter store_raw as attribute instead of filter_state parameter * Fix linting issues --- homeassistant/components/sensor/filter.py | 14 ++++++++++---- tests/components/sensor/test_filter.py | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 3d05dd28e79..92e2cc751ac 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -313,6 +313,7 @@ class Filter: self._entity = entity self._skip_processing = False self._window_size = window_size + self._store_raw = False @property def window_size(self): @@ -337,7 +338,10 @@ class Filter: """Implement a common interface for filters.""" filtered = self._filter_state(FilterState(new_state)) filtered.set_precision(self.precision) - self.states.append(copy(filtered)) + if self._store_raw: + self.states.append(copy(FilterState(new_state))) + else: + self.states.append(copy(filtered)) new_state.state = filtered.state return new_state @@ -402,12 +406,14 @@ class OutlierFilter(Filter): super().__init__(FILTER_NAME_OUTLIER, window_size, precision, entity) self._radius = radius self._stats_internal = Counter() + self._store_raw = True def _filter_state(self, new_state): """Implement the outlier filter.""" + median = statistics.median([s.state for s in self.states]) \ + if self.states else 0 if (len(self.states) == self.states.maxlen and - abs(new_state.state - - statistics.median([s.state for s in self.states])) > + abs(new_state.state - median) > self._radius): self._stats_internal['erasures'] += 1 @@ -415,7 +421,7 @@ class OutlierFilter(Filter): _LOGGER.debug("Outlier nr. %s in %s: %s", self._stats_internal['erasures'], self._entity, new_state) - return self.states[-1] + new_state.state = median return new_state diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index b43d38da5e8..29308f2a83d 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -106,6 +106,23 @@ class TestFilterSensor(unittest.TestCase): precision=2, entity=None, radius=4.0) + for state in self.values: + filtered = filt.filter_state(state) + assert 21 == filtered.state + + def test_outlier_step(self): + """ + Test step-change handling in outlier. + + Test if outlier filter handles long-running step-changes correctly. + It should converge to no longer filter once just over half the + window_size is occupied by the new post step-change values. + """ + filt = OutlierFilter(window_size=3, + precision=2, + entity=None, + radius=1.1) + self.values[-1].state = 22 for state in self.values: filtered = filt.filter_state(state) assert 22 == filtered.state @@ -119,7 +136,7 @@ class TestFilterSensor(unittest.TestCase): out = ha.State('sensor.test_monitored', 4000) for state in [out]+self.values: filtered = filt.filter_state(state) - assert 22 == filtered.state + assert 21 == filtered.state def test_lowpass(self): """Test if lowpass filter works.""" From 02745be44d3558573b2a337764f3de7d8c63e5b3 Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Sat, 23 Feb 2019 09:02:39 -0500 Subject: [PATCH 126/253] Allows the utility_meter to net meter rather than only allow increases. (#21204) * Allow the utility_meter to net meter rather than only allow increases. * Fix PR issues around CI. * Fix line length fallout. * Change rollover to net_consumption. Add unit tests. * Fix test style issues. * Fix style in tests. --- .../components/utility_meter/__init__.py | 7 +- .../components/utility_meter/const.py | 1 + .../components/utility_meter/sensor.py | 15 +++-- tests/components/utility_meter/test_sensor.py | 66 +++++++++++++++++++ 4 files changed, 80 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 3cf1b2fea61..2f062851ee6 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -12,9 +12,9 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from .const import ( DOMAIN, SIGNAL_RESET_METER, METER_TYPES, CONF_SOURCE_SENSOR, - CONF_METER_TYPE, CONF_METER_OFFSET, CONF_TARIFF_ENTITY, CONF_TARIFF, - CONF_TARIFFS, CONF_METER, DATA_UTILITY, SERVICE_RESET, - SERVICE_SELECT_TARIFF, SERVICE_SELECT_NEXT_TARIFF, + CONF_METER_TYPE, CONF_METER_OFFSET, CONF_METER_NET_CONSUMPTION, + CONF_TARIFF_ENTITY, CONF_TARIFF, CONF_TARIFFS, CONF_METER, DATA_UTILITY, + SERVICE_RESET, SERVICE_SELECT_TARIFF, SERVICE_SELECT_NEXT_TARIFF, ATTR_TARIFF) _LOGGER = logging.getLogger(__name__) @@ -36,6 +36,7 @@ METER_CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_METER_TYPE): vol.In(METER_TYPES), vol.Optional(CONF_METER_OFFSET, default=0): cv.positive_int, + vol.Optional(CONF_METER_NET_CONSUMPTION, default=False): cv.boolean, vol.Optional(CONF_TARIFFS, default=[]): vol.All( cv.ensure_list, [cv.string]), }) diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 4d2df0372b5..c5cb6b8aa33 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -15,6 +15,7 @@ CONF_METER = 'meter' CONF_SOURCE_SENSOR = 'source' CONF_METER_TYPE = 'cycle' CONF_METER_OFFSET = 'offset' +CONF_METER_NET_CONSUMPTION = 'net_consumption' CONF_PAUSED = 'paused' CONF_TARIFFS = 'tariffs' CONF_TARIFF = 'tariff' diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index a01c53b20e3..21dc1099442 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -17,7 +17,7 @@ from .const import ( DATA_UTILITY, SIGNAL_RESET_METER, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY, CONF_SOURCE_SENSOR, CONF_METER_TYPE, CONF_METER_OFFSET, - CONF_TARIFF, CONF_TARIFF_ENTITY, CONF_METER) + CONF_METER_NET_CONSUMPTION, CONF_TARIFF, CONF_TARIFF_ENTITY, CONF_METER) _LOGGER = logging.getLogger(__name__) @@ -48,13 +48,15 @@ async def async_setup_platform( conf_meter_source = hass.data[DATA_UTILITY][meter][CONF_SOURCE_SENSOR] conf_meter_type = hass.data[DATA_UTILITY][meter].get(CONF_METER_TYPE) conf_meter_offset = hass.data[DATA_UTILITY][meter][CONF_METER_OFFSET] + conf_meter_net_consumption =\ + hass.data[DATA_UTILITY][meter][CONF_METER_NET_CONSUMPTION] conf_meter_tariff_entity = hass.data[DATA_UTILITY][meter].get( CONF_TARIFF_ENTITY) meters.append(UtilityMeterSensor( conf_meter_source, conf.get(CONF_NAME), conf_meter_type, - conf_meter_offset, conf.get(CONF_TARIFF), - conf_meter_tariff_entity)) + conf_meter_offset, conf_meter_net_consumption, + conf.get(CONF_TARIFF), conf_meter_tariff_entity)) async_add_entities(meters) @@ -62,8 +64,8 @@ async def async_setup_platform( class UtilityMeterSensor(RestoreEntity): """Representation of an utility meter sensor.""" - def __init__(self, source_entity, name, meter_type, meter_offset=0, - tariff=None, tariff_entity=None): + def __init__(self, source_entity, name, meter_type, meter_offset, + net_consumption, tariff=None, tariff_entity=None): """Initialize the Utility Meter sensor.""" self._sensor_source_id = source_entity self._state = 0 @@ -77,6 +79,7 @@ class UtilityMeterSensor(RestoreEntity): self._unit_of_measurement = None self._period = meter_type self._period_offset = meter_offset + self._sensor_net_consumption = net_consumption self._tariff = tariff self._tariff_entity = tariff_entity @@ -96,7 +99,7 @@ class UtilityMeterSensor(RestoreEntity): try: diff = Decimal(new_state.state) - Decimal(old_state.state) - if diff < 0: + if (not self._sensor_net_consumption) and diff < 0: # Source sensor just rolled over for unknow reasons, return self._state += diff diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 23fc8872570..03c95fdf897 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -57,6 +57,72 @@ async def test_state(hass): assert state.state == '1' +async def test_net_consumption(hass): + """Test utility sensor state.""" + config = { + 'utility_meter': { + 'energy_bill': { + 'source': 'sensor.energy', + 'net_consumption': True + } + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id = config[DOMAIN]['energy_bill']['source'] + hass.states.async_set(entity_id, 2, {"unit_of_measurement": "kWh"}) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(seconds=10) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, 1, {"unit_of_measurement": "kWh"}, + force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.energy_bill') + assert state is not None + + assert state.state == '-1' + + +async def test_non_net_consumption(hass): + """Test utility sensor state.""" + config = { + 'utility_meter': { + 'energy_bill': { + 'source': 'sensor.energy', + 'net_consumption': False + } + } + } + + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id = config[DOMAIN]['energy_bill']['source'] + hass.states.async_set(entity_id, 2, {"unit_of_measurement": "kWh"}) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(seconds=10) + with patch('homeassistant.util.dt.utcnow', + return_value=now): + hass.states.async_set(entity_id, 1, {"unit_of_measurement": "kWh"}, + force_update=True) + await hass.async_block_till_done() + + state = hass.states.get('sensor.energy_bill') + assert state is not None + + assert state.state == '0' + + async def _test_self_reset(hass, cycle, start_time, expect_reset=True): """Test energy sensor self reset.""" config = { From 2f7b4ed7f093079a8aa65944f77fdb4dfd9486bd Mon Sep 17 00:00:00 2001 From: tmechen Date: Sat, 23 Feb 2019 15:52:08 +0100 Subject: [PATCH 127/253] implementing freedaily mode (#21314) adressing #15105 and add a freedaily mode for a 5 day forecast with free API key --- homeassistant/components/weather/openweathermap.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index 2100cf169b6..58016dd3e2c 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = 'Data provided by OpenWeatherMap' -FORECAST_MODE = ['hourly', 'daily'] +FORECAST_MODE = ['hourly', 'daily', 'freedaily'] DEFAULT_NAME = 'OpenWeatherMap' @@ -152,7 +152,12 @@ class OpenWeatherMapWeather(WeatherEntity): return None return round(rain_value + snow_value, 1) - for entry in self.forecast_data.get_weathers(): + if self._mode == 'freedaily': + weather = self.forecast_data.get_weathers()[::8] + else: + weather = self.forecast_data.get_weathers() + + for entry in weather: if self._mode == 'daily': data.append({ ATTR_FORECAST_TIME: From 197303b63e75400c097d559f520bf373f5cbd238 Mon Sep 17 00:00:00 2001 From: Benny de Leeuw Date: Sat, 23 Feb 2019 15:57:54 +0100 Subject: [PATCH 128/253] Add voltage per phase (#21319) --- homeassistant/components/sensor/dsmr.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index 8b7d78aa038..1bb7b44cab6 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -147,6 +147,18 @@ async def async_setup_platform(hass, config, async_add_entities, 'Voltage Swells Phase L3', obis_ref.VOLTAGE_SWELL_L3_COUNT ], + [ + 'Voltage Phase L1', + obis_ref.INSTANTANEOUS_VOLTAGE_L1 + ], + [ + 'Voltage Phase L2', + obis_ref.INSTANTANEOUS_VOLTAGE_L2 + ], + [ + 'Voltage Phase L3', + obis_ref.INSTANTANEOUS_VOLTAGE_L3 + ], ] # Generate device entities From f20195ba75f72a025912b09e3796835a828b020b Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sat, 23 Feb 2019 15:58:18 +0100 Subject: [PATCH 129/253] Add conf_key check for LCN platform load (#21354) --- homeassistant/components/lcn/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 6fb9f58a1d6..2f8f9fb4e1c 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -184,10 +184,10 @@ async def async_setup(hass, config): for component, conf_key in (('cover', CONF_COVERS), ('light', CONF_LIGHTS), ('switch', CONF_SWITCHES)): - hass.async_create_task( - async_load_platform(hass, component, DOMAIN, - config[DOMAIN][conf_key], config)) - + if conf_key in config[DOMAIN]: + hass.async_create_task( + async_load_platform(hass, component, DOMAIN, + config[DOMAIN][conf_key], config)) return True From d0279653048703b6b8ce8e7eba3172bd7480ef33 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Sat, 23 Feb 2019 15:28:41 +0000 Subject: [PATCH 130/253] Update luci device tracker (#21321) * * bump pip module version. * moved named tuple into the module. * pass SSL bool into the object init. * support get_extra_attributes * Update homeassistant/components/device_tracker/luci.py --- .../components/device_tracker/luci.py | 34 +++++++++++-------- requirements_all.txt | 2 +- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 27fbf7c9f44..f60e8edd8c4 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -4,7 +4,6 @@ Support for OpenWRT (luci) routers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.luci/ """ -from collections import namedtuple import logging import voluptuous as vol @@ -14,7 +13,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.const import ( CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_SSL) -REQUIREMENTS = ['openwrt-luci-rpc==0.3.0'] +REQUIREMENTS = ['openwrt-luci-rpc==1.0.5'] _LOGGER = logging.getLogger(__name__) @@ -35,23 +34,17 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -Device = namedtuple('Device', ['mac', 'name']) - - class LuciDeviceScanner(DeviceScanner): """This class scans for devices connected to an OpenWrt router.""" def __init__(self, config): """Initialize the scanner.""" - host = config[CONF_HOST] - protocol = 'http' if not config[CONF_SSL] else 'https' - host_url = '{}://{}'.format(protocol, host) - from openwrt_luci_rpc import OpenWrtRpc - self.router = OpenWrtRpc(host_url, + self.router = OpenWrtRpc(config[CONF_HOST], config[CONF_USERNAME], - config[CONF_PASSWORD]) + config[CONF_PASSWORD], + config[CONF_SSL]) self.last_results = {} self.success_init = self.router.is_logged_in() @@ -65,10 +58,24 @@ class LuciDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" name = next(( - result.name for result in self.last_results + result.hostname for result in self.last_results if result.mac == device), None) return name + def get_extra_attributes(self, device): + """ + Get extra attributes of a device. + + Some known extra attributes that may be returned in the device tuple + include Mac Address (mac), Network Device (dev), Ip Address + (ip), reachable status (reachable), Associated router + (host), Hostname if known (hostname) among others. + """ + device = next(( + result for result in self.last_results + if result.mac == device), None) + return device._asdict() + def _update_info(self): """Check the Luci router for devices.""" result = self.router.get_all_connected_devices( @@ -79,7 +86,6 @@ class LuciDeviceScanner(DeviceScanner): last_results = [] for device in result: - last_results.append( - Device(device['macaddress'], device['hostname'])) + last_results.append(device) self.last_results = last_results diff --git a/requirements_all.txt b/requirements_all.txt index 2dc3d8c5be8..4142421b78d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -776,7 +776,7 @@ openhomedevice==0.4.2 opensensemap-api==0.1.4 # homeassistant.components.device_tracker.luci -openwrt-luci-rpc==0.3.0 +openwrt-luci-rpc==1.0.5 # homeassistant.components.switch.orvibo orvibo==1.1.1 From c595cf016f0e6e56d2990a8bc08e9749b8a0edf5 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Sat, 23 Feb 2019 17:13:27 +0000 Subject: [PATCH 131/253] Support the person component in Prometheus (#21363) --- homeassistant/components/prometheus/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 65c1fbc4eb0..9053a872134 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -151,6 +151,15 @@ class PrometheusMetrics: value = state_helper.state_as_number(state) metric.labels(**self._labels(state)).set(value) + def _handle_person(self, state): + metric = self._metric( + 'person_state', + self.prometheus_client.Gauge, + 'State of the person (0/1)', + ) + value = state_helper.state_as_number(state) + metric.labels(**self._labels(state)).set(value) + def _handle_light(self, state): metric = self._metric( 'light_state', From e8b67fc19ff088f0240c3345f8b3920d0674d959 Mon Sep 17 00:00:00 2001 From: yosilevy <37745463+yosilevy@users.noreply.github.com> Date: Sat, 23 Feb 2019 19:26:27 +0200 Subject: [PATCH 132/253] Scene validator fix (#21362) --- homeassistant/components/scene/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 802512dbf5d..8a7934bd694 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -32,7 +32,12 @@ def _platform_validator(config): 'homeassistant.components.scene.{}'.format( config[CONF_PLATFORM])) except ImportError: - raise vol.Invalid('Invalid platform specified') from None + try: + platform = importlib.import_module( + 'homeassistant.components.{}.scene'.format( + config[CONF_PLATFORM])) + except ImportError: + raise vol.Invalid('Invalid platform specified') from None if not hasattr(platform, 'PLATFORM_SCHEMA'): return config From bfda9239996f3868ab548ff19e807ff962d92830 Mon Sep 17 00:00:00 2001 From: Adam Dullage Date: Sat, 23 Feb 2019 19:54:42 +0000 Subject: [PATCH 133/253] Update Starling Bank Integration to v2 API (#21358) * Bump starlingbank to v2 API * Fixed incorrect call --- homeassistant/components/sensor/starlingbank.py | 8 ++++---- requirements_all.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/starlingbank.py b/homeassistant/components/sensor/starlingbank.py index a0c6f23e496..84b30daa2e9 100644 --- a/homeassistant/components/sensor/starlingbank.py +++ b/homeassistant/components/sensor/starlingbank.py @@ -14,7 +14,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['starlingbank==1.2'] +REQUIREMENTS = ['starlingbank==3.0'] _LOGGER = logging.getLogger(__name__) @@ -96,8 +96,8 @@ class StarlingBalanceSensor(Entity): def update(self): """Fetch new state data for the sensor.""" - self._starling_account.balance.update() + self._starling_account.update_balance_data() if self._balance_data_type == 'cleared_balance': - self._state = self._starling_account.balance.cleared_balance + self._state = self._starling_account.cleared_balance / 100 elif self._balance_data_type == 'effective_balance': - self._state = self._starling_account.balance.effective_balance + self._state = self._starling_account.effective_balance / 100 diff --git a/requirements_all.txt b/requirements_all.txt index 4142421b78d..2398fdc977d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1616,7 +1616,7 @@ sqlalchemy==1.2.18 srpenergy==1.0.5 # homeassistant.components.sensor.starlingbank -starlingbank==1.2 +starlingbank==3.0 # homeassistant.components.statsd statsd==3.2.1 From 7143f4e6213959a891b658b8b57a3de124d83aa5 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 23 Feb 2019 22:38:21 +0100 Subject: [PATCH 134/253] Fix person update on create (#21355) * Update tests to set correct hass running state * Update person on adding person if hass is running * Test creating person when hass is running --- homeassistant/components/person/__init__.py | 14 ++++++++++---- tests/components/person/test_init.py | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 3e0b00f7442..f2bca91205c 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -337,12 +337,18 @@ class Person(RestoreEntity): if state: self._parse_source_state(state) - @callback - def person_start_hass(now): + if self.hass.is_running: + # Update person now if hass is already running. self.person_updated() + else: + # Wait for hass start to not have race between person + # and device trackers finishing setup. + @callback + def person_start_hass(now): + self.person_updated() - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, person_start_hass) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, person_start_hass) @callback def person_updated(self): diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index f2d796fb204..6c8c6ebd0dd 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -101,6 +101,7 @@ async def test_valid_invalid_user_ids(hass, hass_admin_user): async def test_setup_tracker(hass, hass_admin_user): """Test set up person with one device tracker.""" + hass.state = CoreState.not_running user_id = hass_admin_user.id config = {DOMAIN: { 'id': '1234', 'name': 'tracked person', 'user_id': user_id, @@ -148,6 +149,7 @@ async def test_setup_tracker(hass, hass_admin_user): async def test_setup_two_trackers(hass, hass_admin_user): """Test set up person with two device trackers.""" + hass.state = CoreState.not_running user_id = hass_admin_user.id config = {DOMAIN: { 'id': '1234', 'name': 'tracked person', 'user_id': user_id, @@ -191,6 +193,7 @@ async def test_setup_two_trackers(hass, hass_admin_user): async def test_ignore_unavailable_states(hass, hass_admin_user): """Test set up person with two device trackers, one unavailable.""" + hass.state = CoreState.not_running user_id = hass_admin_user.id config = {DOMAIN: { 'id': '1234', 'name': 'tracked person', 'user_id': user_id, @@ -234,7 +237,7 @@ async def test_restore_home_state(hass, hass_admin_user): ATTR_SOURCE: DEVICE_TRACKER, ATTR_USER_ID: user_id} state = State('person.tracked_person', 'home', attrs) mock_restore_cache(hass, (state, )) - hass.state = CoreState.starting + hass.state = CoreState.not_running mock_component(hass, 'recorder') config = {DOMAIN: { 'id': '1234', 'name': 'tracked person', 'user_id': user_id, @@ -263,6 +266,21 @@ async def test_duplicate_ids(hass, hass_admin_user): assert hass.states.get('person.test_user_2') is None +async def test_create_person_during_run(hass): + """Test that person is updated if created while hass is running.""" + config = {DOMAIN: {}} + assert await async_setup_component(hass, DOMAIN, config) + hass.states.async_set(DEVICE_TRACKER, 'home') + await hass.async_block_till_done() + + await hass.components.person.async_create_person( + 'tracked person', device_trackers=[DEVICE_TRACKER]) + await hass.async_block_till_done() + + state = hass.states.get('person.tracked_person') + assert state.state == 'home' + + async def test_load_person_storage(hass, hass_admin_user, storage_setup): """Test set up person from storage.""" state = hass.states.get('person.tracked_person') From dc5b8fd8c4c6cc560007e4800a53016520d22e08 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sat, 23 Feb 2019 13:55:08 -0800 Subject: [PATCH 135/253] Split out iperf3 into a component with a sensor platform (#21138) * Move iperf3 sensor to a standalone component * Split out iperf3 into a component with a sensor platform * Update coverage and requirements * Add services.yaml * Clean up a little bit * Lint * Lint --- .coveragerc | 2 +- homeassistant/components/iperf3/__init__.py | 185 +++++++++++++++++ homeassistant/components/iperf3/sensor.py | 100 +++++++++ homeassistant/components/iperf3/services.yaml | 6 + homeassistant/components/sensor/iperf3.py | 195 ------------------ requirements_all.txt | 2 +- 6 files changed, 293 insertions(+), 197 deletions(-) create mode 100644 homeassistant/components/iperf3/__init__.py create mode 100644 homeassistant/components/iperf3/sensor.py create mode 100644 homeassistant/components/iperf3/services.yaml delete mode 100644 homeassistant/components/sensor/iperf3.py diff --git a/.coveragerc b/.coveragerc index 3f36e698dac..494afd35078 100644 --- a/.coveragerc +++ b/.coveragerc @@ -205,6 +205,7 @@ omit = homeassistant/components/insteon/* homeassistant/components/ios/* homeassistant/components/iota/* + homeassistant/components/iperf3/* homeassistant/components/isy994/* homeassistant/components/joaoapps_join/* homeassistant/components/juicenet/* @@ -476,7 +477,6 @@ omit = homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/influxdb.py - homeassistant/components/sensor/iperf3.py homeassistant/components/sensor/irish_rail_transport.py homeassistant/components/sensor/kwb.py homeassistant/components/sensor/lacrosse.py diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py new file mode 100644 index 00000000000..01ac2194f35 --- /dev/null +++ b/homeassistant/components/iperf3/__init__.py @@ -0,0 +1,185 @@ +"""Support for Iperf3 network measurement tool.""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_PORT, \ + CONF_HOST, CONF_PROTOCOL, CONF_HOSTS, CONF_SCAN_INTERVAL +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +REQUIREMENTS = ['iperf3==0.1.10'] + +DOMAIN = 'iperf3' +DATA_UPDATED = '{}_data_updated'.format(DOMAIN) + +_LOGGER = logging.getLogger(__name__) + +CONF_DURATION = 'duration' +CONF_PARALLEL = 'parallel' +CONF_MANUAL = 'manual' + +DEFAULT_DURATION = 10 +DEFAULT_PORT = 5201 +DEFAULT_PARALLEL = 1 +DEFAULT_PROTOCOL = 'tcp' +DEFAULT_INTERVAL = timedelta(minutes=60) + +ATTR_DOWNLOAD = 'download' +ATTR_UPLOAD = 'upload' +ATTR_VERSION = 'Version' +ATTR_HOST = 'host' + +UNIT_OF_MEASUREMENT = 'Mbit/s' + +SENSOR_TYPES = { + ATTR_DOWNLOAD: [ATTR_DOWNLOAD.capitalize(), UNIT_OF_MEASUREMENT], + ATTR_UPLOAD: [ATTR_UPLOAD.capitalize(), UNIT_OF_MEASUREMENT], +} + +PROTOCOLS = ['tcp', 'udp'] + +HOST_CONFIG_SCHEMA = vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Range(5, 10), + vol.Optional(CONF_PARALLEL, default=DEFAULT_PARALLEL): vol.Range(1, 20), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.In(PROTOCOLS), +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOSTS): vol.All( + cv.ensure_list, [HOST_CONFIG_SCHEMA] + ), + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): vol.All( + cv.time_period, cv.positive_timedelta + ), + vol.Optional(CONF_MANUAL, default=False): cv.boolean, + }) +}, extra=vol.ALLOW_EXTRA) + +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_HOST, default=None): cv.string, +}) + + +async def async_setup(hass, config): + """Set up the iperf3 component.""" + import iperf3 + + hass.data[DOMAIN] = {} + + conf = config[DOMAIN] + for host in conf[CONF_HOSTS]: + host_name = host[CONF_HOST] + + client = iperf3.Client() + client.duration = host[CONF_DURATION] + client.server_hostname = host_name + client.port = host[CONF_PORT] + client.num_streams = host[CONF_PARALLEL] + client.protocol = host[CONF_PROTOCOL] + client.verbose = False + + data = hass.data[DOMAIN][host_name] = Iperf3Data(hass, client) + + if not conf[CONF_MANUAL]: + async_track_time_interval( + hass, data.update, conf[CONF_SCAN_INTERVAL] + ) + + def update(call): + """Service call to manually update the data.""" + called_host = call.data[ATTR_HOST] + if called_host in hass.data[DOMAIN]: + hass.data[DOMAIN][called_host].update() + else: + for iperf3_host in hass.data[DOMAIN].values(): + iperf3_host.update() + + hass.services.async_register( + DOMAIN, 'speedtest', update, schema=SERVICE_SCHEMA + ) + + hass.async_create_task( + async_load_platform( + hass, + SENSOR_DOMAIN, + DOMAIN, + conf[CONF_MONITORED_CONDITIONS], + config + ) + ) + + return True + + +class Iperf3Data: + """Get the latest data from iperf3.""" + + def __init__(self, hass, client): + """Initialize the data object.""" + self._hass = hass + self._client = client + self.data = { + ATTR_DOWNLOAD: None, + ATTR_UPLOAD: None, + ATTR_VERSION: None + } + + @property + def protocol(self): + """Return the protocol used for this connection.""" + return self._client.protocol + + @property + def host(self): + """Return the host connected to.""" + return self._client.server_hostname + + @property + def port(self): + """Return the port on the host connected to.""" + return self._client.port + + def update(self, now=None): + """Get the latest data from iperf3.""" + if self.protocol == 'udp': + # UDP only have 1 way attribute + result = self._run_test(ATTR_DOWNLOAD) + self.data[ATTR_DOWNLOAD] = self.data[ATTR_UPLOAD] = getattr( + result, 'Mbps', None) + self.data[ATTR_VERSION] = getattr(result, 'version', None) + else: + result = self._run_test(ATTR_DOWNLOAD) + self.data[ATTR_DOWNLOAD] = getattr( + result, 'received_Mbps', None) + self.data[ATTR_VERSION] = getattr(result, 'version', None) + self.data[ATTR_UPLOAD] = getattr( + self._run_test(ATTR_UPLOAD), 'sent_Mbps', None) + + dispatcher_send(self._hass, DATA_UPDATED, self.host) + + def _run_test(self, test_type): + """Run and return the iperf3 data.""" + self._client.reverse = test_type == ATTR_DOWNLOAD + try: + result = self._client.run() + except (AttributeError, OSError, ValueError) as error: + _LOGGER.error("Iperf3 error: %s", error) + return None + + if result is not None and \ + hasattr(result, 'error') and \ + result.error is not None: + _LOGGER.error("Iperf3 error: %s", result.error) + return None + + return result diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py new file mode 100644 index 00000000000..59813ae0455 --- /dev/null +++ b/homeassistant/components/iperf3/sensor.py @@ -0,0 +1,100 @@ +"""Support for Iperf3 sensors.""" +from homeassistant.components.iperf3 import ( + DATA_UPDATED, DOMAIN as IPERF3_DOMAIN, SENSOR_TYPES, ATTR_VERSION) +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.restore_state import RestoreEntity + +DEPENDENCIES = ['iperf3'] + +ATTRIBUTION = 'Data retrieved using Iperf3' + +ICON = 'mdi:speedometer' + +ATTR_PROTOCOL = 'Protocol' +ATTR_REMOTE_HOST = 'Remote Server' +ATTR_REMOTE_PORT = 'Remote Port' + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info): + """Set up the Iperf3 sensor.""" + sensors = [] + for iperf3_host in hass.data[IPERF3_DOMAIN].values(): + sensors.extend( + [Iperf3Sensor(iperf3_host, sensor) for sensor in discovery_info] + ) + async_add_entities(sensors, True) + + +class Iperf3Sensor(RestoreEntity): + """A Iperf3 sensor implementation.""" + + def __init__(self, iperf3_data, sensor_type): + """Initialize the sensor.""" + self._name = \ + "{} {}".format(SENSOR_TYPES[sensor_type][0], iperf3_data.host) + self._state = None + self._sensor_type = sensor_type + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._iperf3_data = iperf3_data + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_PROTOCOL: self._iperf3_data.protocol, + ATTR_REMOTE_HOST: self._iperf3_data.host, + ATTR_REMOTE_PORT: self._iperf3_data.port, + ATTR_VERSION: self._iperf3_data.data[ATTR_VERSION] + } + + @property + def should_poll(self): + """Return the polling requirement for this sensor.""" + return False + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await super().async_added_to_hass() + state = await self.async_get_last_state() + if not state: + return + self._state = state.state + + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + def update(self): + """Get the latest data and update the states.""" + data = self._iperf3_data.data.get(self._sensor_type) + if data is not None: + self._state = round(data, 2) + + @callback + def _schedule_immediate_update(self, host): + if host == self._iperf3_data.host: + self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/iperf3/services.yaml b/homeassistant/components/iperf3/services.yaml new file mode 100644 index 00000000000..c333d7c74c8 --- /dev/null +++ b/homeassistant/components/iperf3/services.yaml @@ -0,0 +1,6 @@ +speedtest: + description: Immediately take a speedest with iperf3 + fields: + host: + description: The host name of the iperf3 server (already configured) to run a test with. + example: 'iperf.he.net' \ No newline at end of file diff --git a/homeassistant/components/sensor/iperf3.py b/homeassistant/components/sensor/iperf3.py deleted file mode 100644 index 32127c79a91..00000000000 --- a/homeassistant/components/sensor/iperf3.py +++ /dev/null @@ -1,195 +0,0 @@ -""" -Support for Iperf3 network measurement tool. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.iperf3/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_ENTITY_ID, CONF_MONITORED_CONDITIONS, - CONF_HOST, CONF_PORT, CONF_PROTOCOL) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['iperf3==0.1.10'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_PROTOCOL = 'Protocol' -ATTR_REMOTE_HOST = 'Remote Server' -ATTR_REMOTE_PORT = 'Remote Port' -ATTR_VERSION = 'Version' - -ATTRIBUTION = 'Data retrieved using Iperf3' - -CONF_DURATION = 'duration' -CONF_PARALLEL = 'parallel' - -DEFAULT_DURATION = 10 -DEFAULT_PORT = 5201 -DEFAULT_PARALLEL = 1 -DEFAULT_PROTOCOL = 'tcp' - -IPERF3_DATA = 'iperf3' - -SCAN_INTERVAL = timedelta(minutes=60) - -SERVICE_NAME = 'iperf3_update' - -ICON = 'mdi:speedometer' - -SENSOR_TYPES = { - 'download': ['Download', 'Mbit/s'], - 'upload': ['Upload', 'Mbit/s'], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), - vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Range(5, 10), - vol.Optional(CONF_PARALLEL, default=DEFAULT_PARALLEL): vol.Range(1, 20), - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): - vol.In(['tcp', 'udp']), -}) - - -SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.string, -}) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Iperf3 sensor.""" - if hass.data.get(IPERF3_DATA) is None: - hass.data[IPERF3_DATA] = {} - hass.data[IPERF3_DATA]['sensors'] = [] - - dev = [] - for sensor in config[CONF_MONITORED_CONDITIONS]: - dev.append( - Iperf3Sensor(config[CONF_HOST], - config[CONF_PORT], - config[CONF_DURATION], - config[CONF_PARALLEL], - config[CONF_PROTOCOL], - sensor)) - - hass.data[IPERF3_DATA]['sensors'].extend(dev) - add_entities(dev) - - def _service_handler(service): - """Update service for manual updates.""" - entity_id = service.data.get('entity_id') - all_iperf3_sensors = hass.data[IPERF3_DATA]['sensors'] - - for sensor in all_iperf3_sensors: - if entity_id is not None: - if sensor.entity_id == entity_id: - sensor.update() - sensor.schedule_update_ha_state() - break - else: - sensor.update() - sensor.schedule_update_ha_state() - - for sensor in dev: - hass.services.register(DOMAIN, SERVICE_NAME, _service_handler, - schema=SERVICE_SCHEMA) - - -class Iperf3Sensor(Entity): - """A Iperf3 sensor implementation.""" - - def __init__(self, server, port, duration, streams, protocol, sensor_type): - """Initialize the sensor.""" - self._attrs = { - ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_PROTOCOL: protocol, - } - self._name = \ - "{} {}".format(SENSOR_TYPES[sensor_type][0], server) - self._state = None - self._sensor_type = sensor_type - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self._port = port - self._server = server - self._duration = duration - self._num_streams = streams - self._protocol = protocol - self.result = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self.result is not None: - self._attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - self._attrs[ATTR_REMOTE_HOST] = self.result.remote_host - self._attrs[ATTR_REMOTE_PORT] = self.result.remote_port - self._attrs[ATTR_VERSION] = self.result.version - return self._attrs - - def update(self): - """Get the latest data and update the states.""" - import iperf3 - client = iperf3.Client() - client.duration = self._duration - client.server_hostname = self._server - client.port = self._port - client.verbose = False - client.num_streams = self._num_streams - client.protocol = self._protocol - - # when testing download bandwith, reverse must be True - if self._sensor_type == 'download': - client.reverse = True - - try: - self.result = client.run() - except (AttributeError, OSError, ValueError) as error: - self.result = None - _LOGGER.error("Iperf3 sensor error: %s", error) - return - - if self.result is not None and \ - hasattr(self.result, 'error') and \ - self.result.error is not None: - _LOGGER.error("Iperf3 sensor error: %s", self.result.error) - self.result = None - return - - # UDP only have 1 way attribute - if self._protocol == 'udp': - self._state = round(self.result.Mbps, 2) - - elif self._sensor_type == 'download': - self._state = round(self.result.received_Mbps, 2) - - elif self._sensor_type == 'upload': - self._state = round(self.result.sent_Mbps, 2) - - @property - def icon(self): - """Return icon.""" - return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 2398fdc977d..98a8ba19fcd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -577,7 +577,7 @@ influxdb==5.2.0 # homeassistant.components.insteon insteonplm==0.15.2 -# homeassistant.components.sensor.iperf3 +# homeassistant.components.iperf3 iperf3==0.1.10 # homeassistant.components.route53 From a8a2daeac5c9dd423c81e29661f516d3f9be6f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20W?= Date: Sat, 23 Feb 2019 22:55:55 +0100 Subject: [PATCH 136/253] Add custom and zone cleaning to Neato Vacuums (#20779) * Adding custom and zone cleaning to Neato Vacuums * Fixing line length and missing imports * Line too long * Adding details to the custom service * Fix linting issues * Reverting ACTION * Code cleanup * Typo * Requested modifications * Changing the custom service domain * No service schema depency anymore * Removing useless code * Linting * Requested changes * Requested changes for domain * Revert the service domain back to vacuum --- homeassistant/components/neato/__init__.py | 3 + homeassistant/components/neato/vacuum.py | 83 +++++++++++++++++-- homeassistant/components/vacuum/services.yaml | 19 +++++ 3 files changed, 100 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 2b4af3e1e91..bb717b8d230 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -18,6 +18,7 @@ DOMAIN = 'neato' NEATO_ROBOTS = 'neato_robots' NEATO_LOGIN = 'neato_login' NEATO_MAP_DATA = 'neato_map_data' +NEATO_PERSISTENT_MAPS = 'neato_persistent_maps' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -197,6 +198,7 @@ class NeatoHub: domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD]) self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps def login(self): @@ -216,6 +218,7 @@ class NeatoHub: _LOGGER.debug("Running HUB.update_robots %s", self._hass.data[NEATO_ROBOTS]) self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps def download_map(self, url): diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 45cfd273aca..ff78a087de8 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -2,15 +2,21 @@ import logging from datetime import timedelta import requests +import voluptuous as vol +from homeassistant.const import (ATTR_ENTITY_ID) from homeassistant.components.vacuum import ( StateVacuumDevice, SUPPORT_BATTERY, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_STATE, SUPPORT_STOP, SUPPORT_START, STATE_IDLE, STATE_PAUSED, STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, STATE_ERROR, SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON, - SUPPORT_LOCATE, SUPPORT_CLEAN_SPOT) + SUPPORT_LOCATE, SUPPORT_CLEAN_SPOT, DOMAIN) from homeassistant.components.neato import ( - NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS) + NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS, + NEATO_PERSISTENT_MAPS) + +from homeassistant.helpers.service import extract_entity_ids +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -19,8 +25,8 @@ DEPENDENCIES = ['neato'] SCAN_INTERVAL = timedelta(minutes=5) SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ - SUPPORT_STOP | SUPPORT_START | SUPPORT_CLEAN_SPOT | \ - SUPPORT_STATE | SUPPORT_MAP | SUPPORT_LOCATE + SUPPORT_STOP | SUPPORT_START | SUPPORT_CLEAN_SPOT | \ + SUPPORT_STATE | SUPPORT_MAP | SUPPORT_LOCATE ATTR_CLEAN_START = 'clean_start' ATTR_CLEAN_STOP = 'clean_stop' @@ -30,15 +36,56 @@ ATTR_CLEAN_BATTERY_END = 'battery_level_at_clean_end' ATTR_CLEAN_SUSP_COUNT = 'clean_suspension_count' ATTR_CLEAN_SUSP_TIME = 'clean_suspension_time' +ATTR_MODE = 'mode' +ATTR_NAVIGATION = 'navigation' +ATTR_CATEGORY = 'category' +ATTR_ZONE = 'zone' + +SERVICE_NEATO_CUSTOM_CLEANING = 'neato_custom_cleaning' + +SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_MODE, default=2): cv.positive_int, + vol.Optional(ATTR_NAVIGATION, default=1): cv.positive_int, + vol.Optional(ATTR_CATEGORY, default=4): cv.positive_int, + vol.Optional(ATTR_ZONE): cv.string +}) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Neato vacuum.""" dev = [] for robot in hass.data[NEATO_ROBOTS]: dev.append(NeatoConnectedVacuum(hass, robot)) + + if not dev: + return + _LOGGER.debug("Adding vacuums %s", dev) add_entities(dev, True) + def neato_custom_cleaning_service(call): + """Zone cleaning service that allows user to change options.""" + for robot in service_to_entities(call): + if call.service == SERVICE_NEATO_CUSTOM_CLEANING: + mode = call.data.get(ATTR_MODE) + navigation = call.data.get(ATTR_NAVIGATION) + category = call.data.get(ATTR_CATEGORY) + zone = call.data.get(ATTR_ZONE) + robot.neato_custom_cleaning( + mode, navigation, category, zone) + + def service_to_entities(call): + """Return the known devices that a service call mentions.""" + entity_ids = extract_entity_ids(hass, call) + entities = [entity for entity in dev + if entity.entity_id in entity_ids] + return entities + + hass.services.register(DOMAIN, SERVICE_NEATO_CUSTOM_CLEANING, + neato_custom_cleaning_service, + schema=SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA) + class NeatoConnectedVacuum(StateVacuumDevice): """Representation of a Neato Connected Vacuum.""" @@ -62,6 +109,9 @@ class NeatoConnectedVacuum(StateVacuumDevice): self._available = False self._battery_level = None self._robot_serial = self.robot.serial + self._robot_maps = hass.data[NEATO_PERSISTENT_MAPS] + self._robot_boundaries = {} + self._robot_has_map = self.robot.has_persistent_maps def update(self): """Update the states of Neato Vacuums.""" @@ -129,12 +179,18 @@ class NeatoConnectedVacuum(StateVacuumDevice): ['time_in_suspended_cleaning']) self.clean_battery_start = ( self._mapdata[self._robot_serial]['maps'][0]['run_charge_at_start'] - ) + ) self.clean_battery_end = ( self._mapdata[self._robot_serial]['maps'][0]['run_charge_at_end']) self._battery_level = self._state['details']['charge'] + if self._robot_has_map: + robot_map_id = self._robot_maps[self._robot_serial][0]['id'] + + self._robot_boundaries = self.robot.get_map_boundaries( + robot_map_id).json() + @property def name(self): """Return the name of the device.""" @@ -224,3 +280,20 @@ class NeatoConnectedVacuum(StateVacuumDevice): def clean_spot(self, **kwargs): """Run a spot cleaning starting from the base.""" self.robot.start_spot_cleaning() + + def neato_custom_cleaning(self, mode, navigation, category, + zone=None, **kwargs): + """Zone cleaning service call.""" + boundary_id = None + if zone is not None: + for boundary in self._robot_boundaries['data']['boundaries']: + if zone in boundary['name']: + boundary_id = boundary['id'] + if boundary_id is None: + _LOGGER.error( + "Zone '%s' was not found for the robot '%s'", + zone, self._name) + return + + self._clean_state = STATE_CLEANING + self.robot.start_cleaning(mode, navigation, category, boundary_id) diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index 792658bbdfd..fe5bb77cefe 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -144,3 +144,22 @@ xiaomi_clean_zone: repeats: description: Number of cleaning repeats for each zone between 1 and 3. example: '1' + +neato_custom_cleaning: + description: Zone Cleaning service call specific to Neato Botvacs. + fields: + entity_id: + description: Name of the vacuum entity. [Required] + example: 'vacuum.neato' + mode: + description: "Set the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set." + example: 2 + navigation: + description: "Set the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set." + example: 1 + category: + description: "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found)." + example: 2 + zone: + description: Only supported on the Botvac D7. Name of the zone to clean. Defaults to no zone i.e. complete house cleanup. + example: "Kitchen" From 1eba90d2a12e577ecd66590be94da8ab50b024a1 Mon Sep 17 00:00:00 2001 From: Alistair Galbraith Date: Sat, 23 Feb 2019 13:57:10 -0800 Subject: [PATCH 137/253] Add initial support for Sony SDCP projector control (#20269) * Add initial support for Sony SDCP projector control * Changes to reflect code review * Added code per change requests - Validation of connection during setup_platform - Docs pending * Removed blank lines per CI build * Lint fix * Update homeassistant/components/switch/sony_projector.py Co-Authored-By: alistairg * Updated .coveragerc, made requested logger changes * Update docstring --- .coveragerc | 1 + .../components/switch/sony_projector.py | 97 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 101 insertions(+) create mode 100644 homeassistant/components/switch/sony_projector.py diff --git a/.coveragerc b/.coveragerc index 494afd35078..2616a1163bc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -614,6 +614,7 @@ omit = homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/snmp.py + homeassistant/components/switch/sony_projector.py homeassistant/components/switch/switchbot.py homeassistant/components/switch/switchmate.py homeassistant/components/switch/telnet.py diff --git a/homeassistant/components/switch/sony_projector.py b/homeassistant/components/switch/sony_projector.py new file mode 100644 index 00000000000..5b3ffeed75f --- /dev/null +++ b/homeassistant/components/switch/sony_projector.py @@ -0,0 +1,97 @@ +"""Support for Sony projectors via SDCP network control.""" +import logging + +import voluptuous as vol + +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import ( + STATE_ON, STATE_OFF, CONF_NAME, CONF_HOST) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pysdcp==1'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Sony Projector' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Connect to Sony projector using network.""" + import pysdcp + host = config[CONF_HOST] + name = config[CONF_NAME] + sdcp_connection = pysdcp.Projector(host) + + # Sanity check the connection + try: + sdcp_connection.get_power() + except ConnectionError: + _LOGGER.error("Failed to connect to projector '%s'", host) + return False + _LOGGER.debug("Validated projector '%s' OK", host) + add_entities([SonyProjector(sdcp_connection, name)], True) + return True + + +class SonyProjector(SwitchDevice): + """Represents a Sony Projector as a switch.""" + + def __init__(self, sdcp_connection, name): + """Init of the Sony projector.""" + self._sdcp = sdcp_connection + self._name = name + self._state = None + self._available = False + self._attributes = {} + + @property + def available(self): + """Return if projector is available.""" + return self._available + + @property + def name(self): + """Return name of the projector.""" + return self._name + + @property + def is_on(self): + """Return if the projector is turned on.""" + return self._state + + @property + def state_attributes(self): + """Return state attributes.""" + return self._attributes + + def update(self): + """Get the latest state from the projector.""" + try: + self._state = self._sdcp.get_power() + self._available = True + except ConnectionRefusedError: + _LOGGER.error("Projector connection refused") + self._available = False + + def turn_on(self, **kwargs): + """Turn the projector on.""" + _LOGGER.debug("Powering on projector '%s'...", self.name) + if self._sdcp.set_power(True): + _LOGGER.debug("Powered on successfully.") + self._state = STATE_ON + else: + _LOGGER.error("Power on command was not successful") + + def turn_off(self, **kwargs): + """Turn the projector off.""" + _LOGGER.debug("Powering off projector '%s'...", self.name) + if self._sdcp.set_power(False): + _LOGGER.debug("Powered off successfully.") + self._state = STATE_OFF + else: + _LOGGER.error("Power off command was not successful") diff --git a/requirements_all.txt b/requirements_all.txt index 98a8ba19fcd..fb400185fb4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1230,6 +1230,9 @@ pyruter==1.1.0 # homeassistant.components.sabnzbd pysabnzbd==1.1.0 +# homeassistant.components.switch.sony_projector +pysdcp==1 + # homeassistant.components.climate.sensibo pysensibo==1.0.3 From 492c3b24de9668be36d6ef27627dd5e02aadbde5 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 23 Feb 2019 22:57:54 +0100 Subject: [PATCH 138/253] Check for attribute existence for HS220 support (#21309) * Check for attribute existence as smartplug does not have them (for HS220 support) * use getattr over hasattr and a separate check --- homeassistant/components/tplink/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 5d22b1ae60f..1e31df98af5 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -222,11 +222,11 @@ class TPLinkSmartBulb(Light): if self.smartbulb.is_dimmable: self._supported_features += SUPPORT_BRIGHTNESS - if self.smartbulb.is_variable_color_temp: + if getattr(self.smartbulb, 'is_variable_color_temp', False): self._supported_features += SUPPORT_COLOR_TEMP self._min_mireds = kelvin_to_mired( self.smartbulb.valid_temperature_range[1]) self._max_mireds = kelvin_to_mired( self.smartbulb.valid_temperature_range[0]) - if self.smartbulb.is_color: + if getattr(self.smartbulb, 'is_color', False): self._supported_features += SUPPORT_COLOR From a1c3a38428fa284be4873d7343fd85862d487534 Mon Sep 17 00:00:00 2001 From: Thibault Maekelbergh Date: Sat, 23 Feb 2019 22:58:30 +0100 Subject: [PATCH 139/253] Generate new Discogs sensors + fix scan interval (#19443) * Generate new sensors for discogs: - Generate collection sensor - Generate wantlist sensor - Generate random record sensor - Removes the option to set a name * Make it so name can still be configured * Fix invalid syntax * Use shared data object + 1 sensor * Linting * Remove straying comment * Dont use async for non-async stuff * Don't use separate list for conf already in dict * Use consts for keys * Copy dict to list for sensors * Fix syntax for computed keys in SENSORS dict --- homeassistant/components/sensor/discogs.py | 111 +++++++++++++++++---- 1 file changed, 92 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/sensor/discogs.py b/homeassistant/components/sensor/discogs.py index c62cd3e759c..ecbd6d9cab1 100644 --- a/homeassistant/components/sensor/discogs.py +++ b/homeassistant/components/sensor/discogs.py @@ -6,11 +6,13 @@ https://home-assistant.io/components/sensor.discogs/ """ from datetime import timedelta import logging +import random import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_TOKEN +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_TOKEN) from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -25,47 +27,85 @@ ATTRIBUTION = "Data provided by Discogs" DEFAULT_NAME = 'Discogs' -ICON = 'mdi:album' +ICON_RECORD = 'mdi:album' +ICON_PLAYER = 'mdi:record-player' +UNIT_RECORDS = 'records' -SCAN_INTERVAL = timedelta(hours=2) +SCAN_INTERVAL = timedelta(minutes=10) + +SENSOR_COLLECTION_TYPE = 'collection' +SENSOR_WANTLIST_TYPE = 'wantlist' +SENSOR_RANDOM_RECORD_TYPE = 'random_record' + +SENSORS = { + SENSOR_COLLECTION_TYPE: { + 'name': 'Collection', + 'icon': 'mdi:album', + 'unit_of_measurement': 'records' + }, + SENSOR_WANTLIST_TYPE: { + 'name': 'Wantlist', + 'icon': 'mdi:album', + 'unit_of_measurement': 'records' + }, + SENSOR_RANDOM_RECORD_TYPE: { + 'name': 'Random Record', + 'icon': 'mdi:record_player', + 'unit_of_measurement': None + }, +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOKEN): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) }) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Discogs sensor.""" import discogs_client - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + token = config[CONF_TOKEN] + name = config[CONF_NAME] try: - discogs = discogs_client.Client(SERVER_SOFTWARE, user_token=token) - identity = discogs.identity() + discogs_client = discogs_client.Client( + SERVER_SOFTWARE, user_token=token) + + discogs_data = { + 'user': discogs_client.identity().name, + 'folders': discogs_client.identity().collection_folders, + 'collection_count': discogs_client.identity().num_collection, + 'wantlist_count': discogs_client.identity().num_wantlist + } except discogs_client.exceptions.HTTPError: _LOGGER.error("API token is not valid") return - async_add_entities([DiscogsSensor(identity, name)], True) + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + sensors.append(DiscogsSensor(discogs_data, name, sensor_type)) + + add_entities(sensors, True) class DiscogsSensor(Entity): - """Get a user's number of records in collection.""" + """Create a new Discogs sensor for a specific type.""" - def __init__(self, identity, name): + def __init__(self, discogs_data, name, sensor_type): """Initialize the Discogs sensor.""" - self._identity = identity + self._discogs_data = discogs_data self._name = name + self._type = sensor_type self._state = None + self._attrs = {} @property def name(self): """Return the name of the sensor.""" - return self._name + return "{} {}".format(self._name, SENSORS[self._type]['name']) @property def state(self): @@ -75,21 +115,54 @@ class DiscogsSensor(Entity): @property def icon(self): """Return the icon to use in the frontend, if any.""" - return ICON + return SENSORS[self._type]['icon'] @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" - return 'records' + return SENSORS[self._type]['unit_of_measurement'] @property def device_state_attributes(self): """Return the state attributes of the sensor.""" + if self._state is None or self._attrs is None: + return None + + if self._type != SENSOR_RANDOM_RECORD_TYPE: + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_IDENTITY: self._discogs_data['user'], + } + return { + 'cat_no': self._attrs['labels'][0]['catno'], + 'cover_image': self._attrs['cover_image'], + 'format': "{} ({})".format( + self._attrs['formats'][0]['name'], + self._attrs['formats'][0]['descriptions'][0]), + 'label': self._attrs['labels'][0]['name'], + 'released': self._attrs['year'], ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_IDENTITY: self._identity.name, + ATTR_IDENTITY: self._discogs_data['user'], } - async def async_update(self): + def get_random_record(self): + """Get a random record suggestion from the user's collection.""" + # Index 0 in the folders is the 'All' folder + collection = self._discogs_data['folders'][0] + random_index = random.randrange(collection.count) + random_record = collection.releases[random_index].release + + self._attrs = random_record.data + return "{} - {}".format( + random_record.data['artists'][0]['name'], + random_record.data['title']) + + def update(self): """Set state to the amount of records in user's collection.""" - self._state = self._identity.num_collection + if self._type == SENSOR_COLLECTION_TYPE: + self._state = self._discogs_data['collection_count'] + elif self._type == SENSOR_WANTLIST_TYPE: + self._state = self._discogs_data['wantlist_count'] + else: + self._state = self.get_random_record() From 6743ef10abf8295f4470cb7d19679f7d31fd9cca Mon Sep 17 00:00:00 2001 From: Tony763 Date: Sat, 23 Feb 2019 23:39:51 +0100 Subject: [PATCH 140/253] Update panasonic_viera.py (#21365) --- homeassistant/components/media_player/panasonic_viera.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index e5ce22e9524..e6546f7c1e2 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -59,20 +59,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: uuid = None remote = RemoteControl(host, port) - add_entities([PanasonicVieraTVDevice(mac, name, remote, uuid)]) + add_entities([PanasonicVieraTVDevice(mac, name, remote, host, uuid)]) return True host = config.get(CONF_HOST) remote = RemoteControl(host, port) - add_entities([PanasonicVieraTVDevice(mac, name, remote)]) + add_entities([PanasonicVieraTVDevice(mac, name, remote, host)]) return True class PanasonicVieraTVDevice(MediaPlayerDevice): """Representation of a Panasonic Viera TV.""" - def __init__(self, mac, name, remote, uuid=None): + def __init__(self, mac, name, remote, host, uuid=None): """Initialize the Panasonic device.""" import wakeonlan # Save a reference to the imported class @@ -84,6 +84,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): self._playing = True self._state = None self._remote = remote + self._host = host self._volume = 0 @property @@ -140,7 +141,7 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def turn_on(self): """Turn on the media player.""" if self._mac: - self._wol.send_magic_packet(self._mac) + self._wol.send_magic_packet(self._mac, ip_address=self._host) self._state = STATE_ON def turn_off(self): From b588c1fe1c22d78d35d05a0838bb682737ba53c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 24 Feb 2019 00:41:38 +0100 Subject: [PATCH 141/253] Handle connection issue for netatmo (#21346) * Handle connection issue for netatmo * reduce logging --- homeassistant/components/netatmo/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 3f04f22ac76..307b76ca434 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -189,6 +189,13 @@ class NetAtmoSensor(Entity): def update(self): """Get the latest data from NetAtmo API and updates the states.""" self.netatmo_data.update() + if self.netatmo_data.data is None: + if self._state is None: + return + _LOGGER.warning("No data found for %s", self.module_name) + self._state = None + return + data = self.netatmo_data.data.get(self.module_name) if data is None: From fc13e37d8d5e75fdf3e52be4cdea1911909b2ccc Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sun, 24 Feb 2019 01:30:19 +0100 Subject: [PATCH 142/253] Refactoring. Moved LCN constants to const.py (#21376) --- homeassistant/components/lcn/__init__.py | 31 ++++-------------------- homeassistant/components/lcn/const.py | 26 ++++++++++++++++++++ homeassistant/components/lcn/cover.py | 5 ++-- homeassistant/components/lcn/light.py | 5 ++-- homeassistant/components/lcn/switch.py | 6 ++--- 5 files changed, 40 insertions(+), 33 deletions(-) create mode 100644 homeassistant/components/lcn/const.py diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 2f8f9fb4e1c..c7c180737f0 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -1,9 +1,12 @@ """Support for LCN devices.""" import logging -import re import voluptuous as vol +from homeassistant.components.lcn.const import ( + CONF_CONNECTIONS, CONF_DIM_MODE, CONF_DIMMABLE, CONF_MOTOR, CONF_OUTPUT, + CONF_SK_NUM_TRIES, CONF_TRANSITION, DATA_LCN, DEFAULT_NAME, DIM_MODES, + DOMAIN, MOTOR_PORTS, OUTPUT_PORTS, PATTERN_ADDRESS, RELAY_PORTS) from homeassistant.const import ( CONF_ADDRESS, CONF_COVERS, CONF_HOST, CONF_LIGHTS, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SWITCHES, CONF_USERNAME) @@ -11,33 +14,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pypck==0.5.9'] - _LOGGER = logging.getLogger(__name__) -DOMAIN = 'lcn' -DATA_LCN = 'lcn' -DEFAULT_NAME = 'pchk' - -CONF_SK_NUM_TRIES = 'sk_num_tries' -CONF_DIM_MODE = 'dim_mode' -CONF_OUTPUT = 'output' -CONF_TRANSITION = 'transition' -CONF_DIMMABLE = 'dimmable' -CONF_CONNECTIONS = 'connections' -CONF_MOTOR = 'motor' - -DIM_MODES = ['STEPS50', 'STEPS200'] -OUTPUT_PORTS = ['OUTPUT1', 'OUTPUT2', 'OUTPUT3', 'OUTPUT4'] -RELAY_PORTS = ['RELAY1', 'RELAY2', 'RELAY3', 'RELAY4', - 'RELAY5', 'RELAY6', 'RELAY7', 'RELAY8', - 'MOTORONOFF1', 'MOTORUPDOWN1', 'MOTORONOFF2', 'MOTORUPDOWN2', - 'MOTORONOFF3', 'MOTORUPDOWN3', 'MOTORONOFF4', 'MOTORUPDOWN4'] -MOTOR_PORTS = ['MOTOR1', 'MOTOR2', 'MOTOR3', 'MOTOR4'] - -# Regex for address validation -PATTERN_ADDRESS = re.compile('^((?P\\w+)\\.)?s?(?P\\d+)' - '\\.(?Pm|g)?(?P\\d+)$') +REQUIREMENTS = ['pypck==0.5.9'] def has_unique_connection_names(connections): diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py new file mode 100644 index 00000000000..02b35b06797 --- /dev/null +++ b/homeassistant/components/lcn/const.py @@ -0,0 +1,26 @@ +"""Constants for the LCN component.""" +import re + +DOMAIN = 'lcn' +DATA_LCN = 'lcn' +DEFAULT_NAME = 'pchk' + +# Regex for address validation +PATTERN_ADDRESS = re.compile('^((?P\\w+)\\.)?s?(?P\\d+)' + '\\.(?Pm|g)?(?P\\d+)$') + +CONF_CONNECTIONS = 'connections' +CONF_SK_NUM_TRIES = 'sk_num_tries' +CONF_OUTPUT = 'output' +CONF_DIM_MODE = 'dim_mode' +CONF_DIMMABLE = 'dimmable' +CONF_TRANSITION = 'transition' +CONF_MOTOR = 'motor' + +DIM_MODES = ['STEPS50', 'STEPS200'] +OUTPUT_PORTS = ['OUTPUT1', 'OUTPUT2', 'OUTPUT3', 'OUTPUT4'] +RELAY_PORTS = ['RELAY1', 'RELAY2', 'RELAY3', 'RELAY4', + 'RELAY5', 'RELAY6', 'RELAY7', 'RELAY8', + 'MOTORONOFF1', 'MOTORUPDOWN1', 'MOTORONOFF2', 'MOTORUPDOWN2', + 'MOTORONOFF3', 'MOTORUPDOWN3', 'MOTORONOFF4', 'MOTORUPDOWN4'] +MOTOR_PORTS = ['MOTOR1', 'MOTOR2', 'MOTOR3', 'MOTOR4'] diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index 8151144f077..4b4542fd623 100755 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -1,7 +1,8 @@ """Support for LCN covers.""" from homeassistant.components.cover import CoverDevice -from homeassistant.components.lcn import ( - CONF_CONNECTIONS, CONF_MOTOR, DATA_LCN, LcnDevice, get_connection) +from homeassistant.components.lcn import LcnDevice, get_connection +from homeassistant.components.lcn.const import ( + CONF_CONNECTIONS, CONF_MOTOR, DATA_LCN) from homeassistant.const import CONF_ADDRESS DEPENDENCIES = ['lcn'] diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 2b7f4ed4074..5f1008cbd57 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -1,7 +1,8 @@ """Support for LCN lights.""" -from homeassistant.components.lcn import ( +from homeassistant.components.lcn import LcnDevice, get_connection +from homeassistant.components.lcn.const import ( CONF_CONNECTIONS, CONF_DIMMABLE, CONF_OUTPUT, CONF_TRANSITION, DATA_LCN, - OUTPUT_PORTS, LcnDevice, get_connection) + OUTPUT_PORTS) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, Light) diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 60eda2ea779..09f35d26718 100755 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -1,7 +1,7 @@ """Support for LCN switches.""" -from homeassistant.components.lcn import ( - CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS, LcnDevice, - get_connection) +from homeassistant.components.lcn import LcnDevice, get_connection +from homeassistant.components.lcn.const import ( + CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS) from homeassistant.components.switch import SwitchDevice from homeassistant.const import CONF_ADDRESS From d44269981b2ec705b7118fb59b0ca791dee3e6ce Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 24 Feb 2019 02:17:49 +0100 Subject: [PATCH 143/253] Clean up owntracks tests (#21378) --- .../device_tracker/test_owntracks.py | 93 ++++++++++--------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 5e65e0a75c7..1ac3fc4a194 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -1,14 +1,16 @@ """The tests for the Owntracks device tracker.""" import json + from asynctest import patch import pytest -from tests.common import ( - async_fire_mqtt_message, mock_coro, mock_component, - async_mock_mqtt_component, MockConfigEntry) from homeassistant.components import owntracks -from homeassistant.setup import async_setup_component from homeassistant.const import STATE_NOT_HOME +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, + mock_coro) USER = 'greg' DEVICE = 'phone' @@ -45,8 +47,8 @@ FIVE_M = TEST_ZONE_DEG_PER_M * 5.0 # Home Assistant Zones INNER_ZONE = { 'name': 'zone', - 'latitude': TEST_ZONE_LAT+0.1, - 'longitude': TEST_ZONE_LON+0.1, + 'latitude': TEST_ZONE_LAT + 0.1, + 'longitude': TEST_ZONE_LON + 0.1, 'radius': 50 } @@ -271,12 +273,14 @@ BAD_MESSAGE = { BAD_JSON_PREFIX = '--$this is bad json#--' BAD_JSON_SUFFIX = '** and it ends here ^^' +# pylint: disable=invalid-name, len-as-condition, redefined-outer-name + @pytest.fixture -def setup_comp(hass): +def setup_comp(hass, mock_device_tracker_conf): """Initialize components.""" - mock_component(hass, 'group') - mock_component(hass, 'zone') + assert hass.loop.run_until_complete(async_setup_component( + hass, 'persistent_notification', {})) hass.loop.run_until_complete(async_setup_component( hass, 'device_tracker', {})) hass.loop.run_until_complete(async_mock_mqtt_component(hass)) @@ -289,48 +293,42 @@ def setup_comp(hass): hass.states.async_set( 'zone.outer', 'zoning', OUTER_ZONE) + yield async def setup_owntracks(hass, config, ctx_cls=owntracks.OwnTracksContext): """Set up OwnTracks.""" - await async_mock_mqtt_component(hass) - MockConfigEntry(domain='owntracks', data={ 'webhook_id': 'owntracks_test', 'secret': 'abcd', }).add_to_hass(hass) - with patch('homeassistant.components.device_tracker.async_load_config', - return_value=mock_coro([])), \ - patch('homeassistant.components.device_tracker.' - 'load_yaml_config_file', return_value=mock_coro({})), \ - patch.object(owntracks, 'OwnTracksContext', ctx_cls): + with patch.object(owntracks, 'OwnTracksContext', ctx_cls): assert await async_setup_component( hass, 'owntracks', {'owntracks': config}) + await hass.async_block_till_done() @pytest.fixture def context(hass, setup_comp): """Set up the mocked context.""" - patcher = patch('homeassistant.components.device_tracker.' - 'DeviceTracker.async_update_config') - patcher.start() - orig_context = owntracks.OwnTracksContext - context = None + # pylint: disable=no-value-for-parameter + def store_context(*args): + """Store the context.""" nonlocal context context = orig_context(*args) return context hass.loop.run_until_complete(setup_owntracks(hass, { - CONF_MAX_GPS_ACCURACY: 200, - CONF_WAYPOINT_IMPORT: True, - CONF_WAYPOINT_WHITELIST: ['jon', 'greg'] - }, store_context)) + CONF_MAX_GPS_ACCURACY: 200, + CONF_WAYPOINT_IMPORT: True, + CONF_WAYPOINT_WHITELIST: ['jon', 'greg'] + }, store_context)) def get_context(): """Get the current context.""" @@ -338,8 +336,6 @@ def context(hass, setup_comp): yield get_context - patcher.stop() - async def send_message(hass, topic, message, corrupt=False): """Test the sending of a message.""" @@ -851,7 +847,7 @@ async def test_event_beacon_unknown_zone_no_location(hass, context): # that will be tracked at my current location. Except # in this case my Device hasn't had a location message # yet so it's in an odd state where it has state.state - # None and no GPS coords so set the beacon to. + # None and no GPS coords to set the beacon to. hass.states.async_set(DEVICE_TRACKER_STATE, None) message = build_message( @@ -993,8 +989,7 @@ async def test_mobile_multiple_async_enter_exit(hass, context): await hass.async_block_till_done() await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_LEAVE_EVENT_MESSAGE) - assert len(context().mobile_beacons_active['greg_phone']) == \ - 0 + assert len(context().mobile_beacons_active['greg_phone']) == 0 async def test_mobile_multiple_enter_exit(hass, context): @@ -1003,8 +998,7 @@ async def test_mobile_multiple_enter_exit(hass, context): await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_ENTER_EVENT_MESSAGE) await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_LEAVE_EVENT_MESSAGE) - assert len(context().mobile_beacons_active['greg_phone']) == \ - 0 + assert len(context().mobile_beacons_active['greg_phone']) == 0 async def test_complex_movement(hass, context): @@ -1153,38 +1147,46 @@ async def test_complex_movement_sticky_keys_beacon(hass, context): # leave keys await send_message(hass, LOCATION_TOPIC, location_message) await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_LEAVE_EVENT_MESSAGE) + assert_location_latitude(hass, INNER_ZONE['latitude']) assert_location_state(hass, 'inner') assert_mobile_tracker_state(hass, 'inner') + assert_mobile_tracker_latitude(hass, INNER_ZONE['latitude']) # leave inner region beacon await send_message(hass, EVENT_TOPIC, REGION_BEACON_LEAVE_MESSAGE) await send_message(hass, LOCATION_TOPIC, location_message) assert_location_state(hass, 'inner') assert_mobile_tracker_state(hass, 'inner') + assert_mobile_tracker_latitude(hass, INNER_ZONE['latitude']) # enter inner region beacon await send_message(hass, EVENT_TOPIC, REGION_BEACON_ENTER_MESSAGE) await send_message(hass, LOCATION_TOPIC, location_message) assert_location_latitude(hass, INNER_ZONE['latitude']) assert_location_state(hass, 'inner') + assert_mobile_tracker_state(hass, 'inner') + assert_mobile_tracker_latitude(hass, INNER_ZONE['latitude']) # enter keys await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_ENTER_EVENT_MESSAGE) await send_message(hass, LOCATION_TOPIC, location_message) assert_location_state(hass, 'inner') assert_mobile_tracker_state(hass, 'inner') + assert_mobile_tracker_latitude(hass, INNER_ZONE['latitude']) # leave keys await send_message(hass, LOCATION_TOPIC, location_message) await send_message(hass, EVENT_TOPIC, MOBILE_BEACON_LEAVE_EVENT_MESSAGE) assert_location_state(hass, 'inner') assert_mobile_tracker_state(hass, 'inner') + assert_mobile_tracker_latitude(hass, INNER_ZONE['latitude']) # leave inner region beacon await send_message(hass, EVENT_TOPIC, REGION_BEACON_LEAVE_MESSAGE) await send_message(hass, LOCATION_TOPIC, location_message) assert_location_state(hass, 'inner') assert_mobile_tracker_state(hass, 'inner') + assert_mobile_tracker_latitude(hass, INNER_ZONE['latitude']) # GPS leave inner region, I'm in the 'outer' region now # but on GPS coords @@ -1222,7 +1224,7 @@ async def test_waypoint_import_blacklist(hass, context): assert wayp is None -async def test_waypoint_import_no_whitelist(hass, config_context): +async def test_waypoint_import_no_whitelist(hass, setup_comp): """Test import of list of waypoints with no whitelist set.""" await setup_owntracks(hass, { CONF_MAX_GPS_ACCURACY: 200, @@ -1296,7 +1298,6 @@ def generate_ciphers(secret): # libnacl ciphertext generation will fail if the module # cannot be imported. However, the test for decryption # also relies on this library and won't be run without it. - import json import pickle import base64 @@ -1304,9 +1305,8 @@ def generate_ciphers(secret): from libnacl import crypto_secretbox_KEYBYTES as KEYLEN from libnacl.secret import SecretBox key = secret.encode("utf-8")[:KEYLEN].ljust(KEYLEN, b'\0') - ctxt = base64.b64encode(SecretBox(key).encrypt( - json.dumps(DEFAULT_LOCATION_MESSAGE).encode("utf-8")) - ).decode("utf-8") + ctxt = base64.b64encode(SecretBox(key).encrypt(json.dumps( + DEFAULT_LOCATION_MESSAGE).encode("utf-8"))).decode("utf-8") except (ImportError, OSError): ctxt = '' @@ -1368,7 +1368,7 @@ def config_context(hass, setup_comp): @patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) -async def test_encrypted_payload(hass, config_context): +async def test_encrypted_payload(hass, setup_comp): """Test encrypted payload.""" await setup_owntracks(hass, { CONF_SECRET: TEST_SECRET_KEY, @@ -1379,7 +1379,7 @@ async def test_encrypted_payload(hass, config_context): @patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) -async def test_encrypted_payload_topic_key(hass, config_context): +async def test_encrypted_payload_topic_key(hass, setup_comp): """Test encrypted payload with a topic key.""" await setup_owntracks(hass, { CONF_SECRET: { @@ -1392,7 +1392,7 @@ async def test_encrypted_payload_topic_key(hass, config_context): @patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) -async def test_encrypted_payload_no_key(hass, config_context): +async def test_encrypted_payload_no_key(hass, setup_comp): """Test encrypted payload with no key, .""" assert hass.states.get(DEVICE_TRACKER_STATE) is None await setup_owntracks(hass, { @@ -1405,7 +1405,7 @@ async def test_encrypted_payload_no_key(hass, config_context): @patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) -async def test_encrypted_payload_wrong_key(hass, config_context): +async def test_encrypted_payload_wrong_key(hass, setup_comp): """Test encrypted payload with wrong key.""" await setup_owntracks(hass, { CONF_SECRET: 'wrong key', @@ -1416,7 +1416,7 @@ async def test_encrypted_payload_wrong_key(hass, config_context): @patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) -async def test_encrypted_payload_wrong_topic_key(hass, config_context): +async def test_encrypted_payload_wrong_topic_key(hass, setup_comp): """Test encrypted payload with wrong topic key.""" await setup_owntracks(hass, { CONF_SECRET: { @@ -1429,7 +1429,7 @@ async def test_encrypted_payload_wrong_topic_key(hass, config_context): @patch('homeassistant.components.owntracks.device_tracker.get_cipher', mock_cipher) -async def test_encrypted_payload_no_topic_key(hass, config_context): +async def test_encrypted_payload_no_topic_key(hass, setup_comp): """Test encrypted payload with no topic key.""" await setup_owntracks(hass, { CONF_SECRET: { @@ -1439,9 +1439,10 @@ async def test_encrypted_payload_no_topic_key(hass, config_context): assert hass.states.get(DEVICE_TRACKER_STATE) is None -async def test_encrypted_payload_libsodium(hass, config_context): +async def test_encrypted_payload_libsodium(hass, setup_comp): """Test sending encrypted message payload.""" try: + # pylint: disable=unused-import import libnacl # noqa: F401 except (ImportError, OSError): pytest.skip("libnacl/libsodium is not installed") @@ -1455,7 +1456,7 @@ async def test_encrypted_payload_libsodium(hass, config_context): assert_location_latitude(hass, LOCATION_MESSAGE['lat']) -async def test_customized_mqtt_topic(hass, config_context): +async def test_customized_mqtt_topic(hass, setup_comp): """Test subscribing to a custom mqtt topic.""" await setup_owntracks(hass, { CONF_MQTT_TOPIC: 'mytracks/#', @@ -1467,7 +1468,7 @@ async def test_customized_mqtt_topic(hass, config_context): assert_location_latitude(hass, LOCATION_MESSAGE['lat']) -async def test_region_mapping(hass, config_context): +async def test_region_mapping(hass, setup_comp): """Test region to zone mapping.""" await setup_owntracks(hass, { CONF_REGION_MAPPING: { From ce86fe47e3337d384c38338dd3fb442f152576a7 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sat, 23 Feb 2019 17:24:38 -0800 Subject: [PATCH 144/253] Remove the google travel time update service (#21153) --- .../components/sensor/google_travel_time.py | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/sensor/google_travel_time.py b/homeassistant/components/sensor/google_travel_time.py index 6c197475653..1f4d8425d6e 100644 --- a/homeassistant/components/sensor/google_travel_time.py +++ b/homeassistant/components/sensor/google_travel_time.py @@ -4,21 +4,21 @@ Support for Google travel time sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.google_travel_time/ """ +import logging from datetime import datetime from datetime import timedelta -import logging import voluptuous as vol -from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_API_KEY, CONF_NAME, EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_MODE) -from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import location -import homeassistant.util.dt as dt_util +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle REQUIREMENTS = ['googlemaps==2.5.1'] @@ -83,18 +83,16 @@ def convert_time_to_utc(timestr): def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the Google travel time platform.""" def run_setup(event): - """Delay the setup until Home Assistant is fully initialized. + """ + Delay the setup until Home Assistant is fully initialized. This allows any entities to be created already """ + hass.data.setdefault(DATA_KEY, []) options = config.get(CONF_OPTIONS) if options.get('units') is None: options['units'] = hass.config.units.name - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = [] - hass.services.register( - DOMAIN, 'google_travel_sensor_update', update) travel_mode = config.get(CONF_TRAVEL_MODE) mode = options.get(CONF_MODE) @@ -120,14 +118,6 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): if sensor.valid_api_connection: add_entities_callback([sensor]) - def update(service): - """Update service for manual updates.""" - entity_id = service.data.get('entity_id') - for sensor in hass.data[DATA_KEY]: - if sensor.entity_id == entity_id: - sensor.update(no_throttle=True) - sensor.schedule_update_ha_state() - # Wait until start event is sent to load this component. hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) From 3d9f4bf2aac73e9ab69fea9c8326fce8a37270ad Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sat, 23 Feb 2019 19:52:37 -0600 Subject: [PATCH 145/253] SmartThings Lock platform state attributes enhancement (#21379) * Add additional lock metadata * Fixed attribute name in test --- homeassistant/components/smartthings/lock.py | 5 ++++- tests/components/smartthings/test_lock.py | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py index bc5ab7a8ccd..c7ab091454c 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -10,9 +10,12 @@ DEPENDENCIES = ['smartthings'] ST_STATE_LOCKED = 'locked' ST_LOCK_ATTR_MAP = { - 'method': 'method', 'codeId': 'code_id', + 'codeName': 'code_name', + 'lockName': 'lock_name', + 'method': 'method', 'timeout': 'timeout', + 'usedCode': 'used_code' } diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 3739a2dc9b5..922abbb161f 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -5,6 +5,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ from pysmartthings import Attribute, Capability +from pysmartthings.device import Status from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.smartthings import lock @@ -45,8 +46,15 @@ async def test_entity_and_device_attributes(hass, device_factory): async def test_lock(hass, device_factory): """Test the lock locks successfully.""" # Arrange - device = device_factory('Lock_1', [Capability.lock], - {Attribute.lock: 'unlocked'}) + device = device_factory('Lock_1', [Capability.lock]) + device.status.attributes[Attribute.lock] = Status( + 'unlocked', None, { + 'method': 'Manual', + 'codeId': None, + 'codeName': 'Code 1', + 'lockName': 'Front Door', + 'usedCode': 'Code 2' + }) await setup_platform(hass, LOCK_DOMAIN, device) # Act await hass.services.async_call( @@ -56,6 +64,12 @@ async def test_lock(hass, device_factory): state = hass.states.get('lock.lock_1') assert state is not None assert state.state == 'locked' + assert state.attributes['method'] == 'Manual' + assert state.attributes['lock_state'] == 'locked' + assert state.attributes['code_name'] == 'Code 1' + assert state.attributes['used_code'] == 'Code 2' + assert state.attributes['lock_name'] == 'Front Door' + assert 'code_id' not in state.attributes async def test_unlock(hass, device_factory): From 2ada0ecfd9fad916bfb51937709876c0c6997be0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 24 Feb 2019 05:14:26 +0100 Subject: [PATCH 146/253] Upgrade shodan to 1.11.0 (#21384) --- homeassistant/components/sensor/shodan.py | 9 ++------- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py index 1cce17cf64a..234631df5f2 100644 --- a/homeassistant/components/sensor/shodan.py +++ b/homeassistant/components/sensor/shodan.py @@ -1,9 +1,4 @@ -""" -Sensor for displaying the number of result on Shodan.io. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.shodan/ -""" +"""Sensor for displaying the number of result on Shodan.io.""" import logging from datetime import timedelta @@ -14,7 +9,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['shodan==1.10.4'] +REQUIREMENTS = ['shodan==1.11.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fb400185fb4..a89deb3d1f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1550,7 +1550,7 @@ sense_energy==0.6.0 sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan -shodan==1.10.4 +shodan==1.11.0 # homeassistant.components.notify.simplepush simplepush==1.1.4 From 3732d75633e33d8abafd99146d11756eeb9b102e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 24 Feb 2019 10:22:17 +0100 Subject: [PATCH 147/253] Update ordering (#21377) --- homeassistant/components/modbus/__init__.py | 82 +++++++------------ .../components/modbus/binary_sensor.py | 15 ++-- homeassistant/components/modbus/climate.py | 50 ++++++----- homeassistant/components/modbus/sensor.py | 72 +++++++--------- homeassistant/components/modbus/switch.py | 52 ++++++------ 5 files changed, 119 insertions(+), 152 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index f42423bf9a8..182e3dc28fa 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -4,27 +4,34 @@ import threading import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - CONF_HOST, CONF_METHOD, CONF_NAME, CONF_PORT, CONF_TYPE, CONF_TIMEOUT, - ATTR_STATE) - -DOMAIN = 'modbus' + ATTR_STATE, CONF_HOST, CONF_METHOD, CONF_NAME, CONF_PORT, CONF_TIMEOUT, + CONF_TYPE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pymodbus==1.5.2'] -CONF_HUB = 'hub' -# Type of network +_LOGGER = logging.getLogger(__name__) + +ATTR_ADDRESS = 'address' +ATTR_HUB = 'hub' +ATTR_UNIT = 'unit' +ATTR_VALUE = 'value' + CONF_BAUDRATE = 'baudrate' CONF_BYTESIZE = 'bytesize' -CONF_STOPBITS = 'stopbits' +CONF_HUB = 'hub' CONF_PARITY = 'parity' +CONF_STOPBITS = 'stopbits' DEFAULT_HUB = 'default' +DOMAIN = 'modbus' + +SERVICE_WRITE_COIL = 'write_coil' +SERVICE_WRITE_REGISTER = 'write_register' BASE_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string + vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string, }) SERIAL_SCHEMA = BASE_SCHEMA.extend({ @@ -49,16 +56,6 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)]) }, extra=vol.ALLOW_EXTRA,) -_LOGGER = logging.getLogger(__name__) - -SERVICE_WRITE_REGISTER = 'write_register' -SERVICE_WRITE_COIL = 'write_coil' - -ATTR_ADDRESS = 'address' -ATTR_HUB = 'hub' -ATTR_UNIT = 'unit' -ATTR_VALUE = 'value' - SERVICE_WRITE_REGISTER_SCHEMA = vol.Schema({ vol.Optional(ATTR_HUB, default=DEFAULT_HUB): cv.string, vol.Required(ATTR_UNIT): cv.positive_int, @@ -109,14 +106,13 @@ def setup_client(client_config): def setup(hass, config): """Set up Modbus component.""" - # Modbus connection type hass.data[DOMAIN] = hub_collect = {} for client_config in config[DOMAIN]: client = setup_client(client_config) name = client_config[CONF_NAME] hub_collect[name] = ModbusHub(client, name) - _LOGGER.debug('Setting up hub: %s', client_config) + _LOGGER.debug("Setting up hub: %s", client_config) def stop_modbus(event): """Stop Modbus service.""" @@ -139,24 +135,20 @@ def setup(hass, config): schema=SERVICE_WRITE_COIL_SCHEMA) def write_register(service): - """Write modbus registers.""" + """Write Modbus registers.""" unit = int(float(service.data.get(ATTR_UNIT))) address = int(float(service.data.get(ATTR_ADDRESS))) value = service.data.get(ATTR_VALUE) client_name = service.data.get(ATTR_HUB) if isinstance(value, list): hub_collect[client_name].write_registers( - unit, - address, - [int(float(i)) for i in value]) + unit, address, [int(float(i)) for i in value]) else: hub_collect[client_name].write_register( - unit, - address, - int(float(value))) + unit, address, int(float(value))) def write_coil(service): - """Write modbus coil.""" + """Write Modbus coil.""" unit = service.data.get(ATTR_UNIT) address = service.data.get(ATTR_ADDRESS) state = service.data.get(ATTR_STATE) @@ -172,7 +164,7 @@ class ModbusHub: """Thread safe wrapper class for pymodbus.""" def __init__(self, modbus_client, name): - """Initialize the modbus hub.""" + """Initialize the Modbus hub.""" self._client = modbus_client self._lock = threading.Lock() self._name = name @@ -196,52 +188,36 @@ class ModbusHub: """Read coils.""" with self._lock: kwargs = {'unit': unit} if unit else {} - return self._client.read_coils( - address, - count, - **kwargs) + return self._client.read_coils(address, count, **kwargs) def read_input_registers(self, unit, address, count): """Read input registers.""" with self._lock: kwargs = {'unit': unit} if unit else {} return self._client.read_input_registers( - address, - count, - **kwargs) + address, count, **kwargs) def read_holding_registers(self, unit, address, count): """Read holding registers.""" with self._lock: kwargs = {'unit': unit} if unit else {} return self._client.read_holding_registers( - address, - count, - **kwargs) + address, count, **kwargs) def write_coil(self, unit, address, value): """Write coil.""" with self._lock: kwargs = {'unit': unit} if unit else {} - self._client.write_coil( - address, - value, - **kwargs) + self._client.write_coil(address, value, **kwargs) def write_register(self, unit, address, value): """Write register.""" with self._lock: kwargs = {'unit': unit} if unit else {} - self._client.write_register( - address, - value, - **kwargs) + self._client.write_register(address, value, **kwargs) def write_registers(self, unit, address, values): """Write registers.""" with self._lock: kwargs = {'unit': unit} if unit else {} - self._client.write_registers( - address, - values, - **kwargs) + self._client.write_registers(address, values, **kwargs) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 38511ffed7e..4e0ab74445d 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -1,25 +1,27 @@ """Support for Modbus Coil sensors.""" import logging + import voluptuous as vol +from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) -from homeassistant.const import CONF_NAME, CONF_SLAVE -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.helpers import config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, CONF_SLAVE +from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['modbus'] CONF_COIL = 'coil' CONF_COILS = 'coils' +DEPENDENCIES = ['modbus'] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COILS): [{ - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_COIL): cv.positive_int, vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_SLAVE): cv.positive_int, }] }) @@ -33,6 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors.append(ModbusCoilSensor( hub, coil.get(CONF_NAME), coil.get(CONF_SLAVE), coil.get(CONF_COIL))) + add_entities(sensors) @@ -40,7 +43,7 @@ class ModbusCoilSensor(BinarySensorDevice): """Modbus coil sensor.""" def __init__(self, hub, name, slave, coil): - """Initialize the modbus coil sensor.""" + """Initialize the Modbus coil sensor.""" self._hub = hub self._name = name self._slave = int(slave) if slave else None diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 860816cb63e..44daedac9c1 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -4,20 +4,15 @@ import struct import voluptuous as vol -from homeassistant.const import ( - CONF_NAME, CONF_SLAVE, ATTR_TEMPERATURE) -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA -from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) +from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, CONF_SLAVE import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['modbus'] - -# Parameters not defined by homeassistant.const CONF_TARGET_TEMP = 'target_temp_register' CONF_CURRENT_TEMP = 'current_temp_register' CONF_DATA_TYPE = 'data_type' @@ -27,21 +22,22 @@ CONF_PRECISION = 'precision' DATA_TYPE_INT = 'int' DATA_TYPE_UINT = 'uint' DATA_TYPE_FLOAT = 'float' +DEPENDENCIES = ['modbus'] + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, + vol.Required(CONF_CURRENT_TEMP): cv.positive_int, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_SLAVE): cv.positive_int, vol.Required(CONF_TARGET_TEMP): cv.positive_int, - vol.Required(CONF_CURRENT_TEMP): cv.positive_int, + vol.Optional(CONF_COUNT, default=2): cv.positive_int, vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]), - vol.Optional(CONF_COUNT, default=2): cv.positive_int, - vol.Optional(CONF_PRECISION, default=1): cv.positive_int + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, + vol.Optional(CONF_PRECISION, default=1): cv.positive_int, }) -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Modbus Thermostat Platform.""" @@ -78,12 +74,14 @@ class ModbusThermostat(ClimateDevice): self._precision = precision self._structure = '>f' - data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'}, - DATA_TYPE_UINT: {1: 'H', 2: 'I', 4: 'Q'}, - DATA_TYPE_FLOAT: {1: 'e', 2: 'f', 4: 'd'}} + data_types = { + DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'}, + DATA_TYPE_UINT: {1: 'H', 2: 'I', 4: 'Q'}, + DATA_TYPE_FLOAT: {1: 'e', 2: 'f', 4: 'd'}, + } - self._structure = '>{}'.format(data_types[self._data_type] - [self._count]) + self._structure = '>{}'.format( + data_types[self._data_type][self._count]) @property def supported_features(self): @@ -109,7 +107,7 @@ class ModbusThermostat(ClimateDevice): @property def target_temperature(self): - """Return the temperature we try to reach.""" + """Return the target temperature.""" return self._target_temperature def set_temperature(self, **kwargs): @@ -121,16 +119,16 @@ class ModbusThermostat(ClimateDevice): register_value = struct.unpack('>h', byte_string[0:2])[0] try: - self.write_register(self._target_temperature_register, - register_value) + self.write_register( + self._target_temperature_register, register_value) except AttributeError as ex: _LOGGER.error(ex) def read_register(self, register): - """Read holding register using the modbus hub slave.""" + """Read holding register using the Modbus hub slave.""" try: - result = self._hub.read_holding_registers(self._slave, register, - self._count) + result = self._hub.read_holding_registers( + self._slave, register, self._count) except AttributeError as ex: _LOGGER.error(ex) byte_string = b''.join( @@ -140,5 +138,5 @@ class ModbusThermostat(ClimateDevice): return register_value def write_register(self, register, value): - """Write register using the modbus hub slave.""" + """Write register using the Modbus hub slave.""" self._hub.write_registers(self._slave, register, [value, 0]) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 6ba8d92d155..3f8c68b25ff 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -6,50 +6,50 @@ import voluptuous as vol from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) -from homeassistant.const import ( - CONF_NAME, CONF_OFFSET, CONF_UNIT_OF_MEASUREMENT, CONF_SLAVE, - CONF_STRUCTURE) -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers import config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, CONF_OFFSET, CONF_SLAVE, CONF_STRUCTURE, + CONF_UNIT_OF_MEASUREMENT) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['modbus'] - CONF_COUNT = 'count' -CONF_REVERSE_ORDER = 'reverse_order' +CONF_DATA_TYPE = 'data_type' CONF_PRECISION = 'precision' CONF_REGISTER = 'register' -CONF_REGISTERS = 'registers' -CONF_SCALE = 'scale' -CONF_DATA_TYPE = 'data_type' CONF_REGISTER_TYPE = 'register_type' +CONF_REGISTERS = 'registers' +CONF_REVERSE_ORDER = 'reverse_order' +CONF_SCALE = 'scale' + +DATA_TYPE_CUSTOM = 'custom' +DATA_TYPE_FLOAT = 'float' +DATA_TYPE_INT = 'int' +DATA_TYPE_UINT = 'uint' + +DEPENDENCIES = ['modbus'] REGISTER_TYPE_HOLDING = 'holding' REGISTER_TYPE_INPUT = 'input' -DATA_TYPE_INT = 'int' -DATA_TYPE_UINT = 'uint' -DATA_TYPE_FLOAT = 'float' -DATA_TYPE_CUSTOM = 'custom' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_REGISTERS): [{ - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_REGISTER): cv.positive_int, - vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): - vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]), vol.Optional(CONF_COUNT, default=1): cv.positive_int, - vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean, - vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), - vol.Optional(CONF_PRECISION, default=0): cv.positive_int, - vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), - vol.Optional(CONF_SLAVE): cv.positive_int, vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, DATA_TYPE_CUSTOM]), + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, + vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), + vol.Optional(CONF_PRECISION, default=0): cv.positive_int, + vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): + vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]), + vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean, + vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), + vol.Optional(CONF_SLAVE): cv.positive_int, vol.Optional(CONF_STRUCTURE): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, }] @@ -93,17 +93,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hub_name = register.get(CONF_HUB) hub = hass.data[MODBUS_DOMAIN][hub_name] sensors.append(ModbusRegisterSensor( - hub, - register.get(CONF_NAME), - register.get(CONF_SLAVE), - register.get(CONF_REGISTER), - register.get(CONF_REGISTER_TYPE), - register.get(CONF_UNIT_OF_MEASUREMENT), - register.get(CONF_COUNT), - register.get(CONF_REVERSE_ORDER), - register.get(CONF_SCALE), - register.get(CONF_OFFSET), - structure, + hub, register.get(CONF_NAME), register.get(CONF_SLAVE), + register.get(CONF_REGISTER), register.get(CONF_REGISTER_TYPE), + register.get(CONF_UNIT_OF_MEASUREMENT), register.get(CONF_COUNT), + register.get(CONF_REVERSE_ORDER), register.get(CONF_SCALE), + register.get(CONF_OFFSET), structure, register.get(CONF_PRECISION))) if not sensors: @@ -158,14 +152,10 @@ class ModbusRegisterSensor(RestoreEntity): """Update the state of the sensor.""" if self._register_type == REGISTER_TYPE_INPUT: result = self._hub.read_input_registers( - self._slave, - self._register, - self._count) + self._slave, self._register, self._count) else: result = self._hub.read_holding_registers( - self._slave, - self._register, - self._count) + self._slave, self._register, self._count) val = 0 try: diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 47ad8e98958..b7039a01da3 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -1,54 +1,54 @@ """Support for Modbus switches.""" import logging + import voluptuous as vol from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN) +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_SLAVE, CONF_COMMAND_ON, CONF_COMMAND_OFF, STATE_ON) + CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_NAME, CONF_SLAVE, STATE_ON) +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers import config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['modbus'] +CONF_COIL = 'coil' +CONF_COILS = 'coils' +CONF_REGISTER = 'register' +CONF_REGISTER_TYPE = 'register_type' +CONF_REGISTERS = 'registers' +CONF_STATE_OFF = 'state_off' +CONF_STATE_ON = 'state_on' +CONF_VERIFY_REGISTER = 'verify_register' +CONF_VERIFY_STATE = 'verify_state' -CONF_COIL = "coil" -CONF_COILS = "coils" -CONF_REGISTER = "register" -CONF_REGISTERS = "registers" -CONF_VERIFY_STATE = "verify_state" -CONF_VERIFY_REGISTER = "verify_register" -CONF_REGISTER_TYPE = "register_type" -CONF_STATE_ON = "state_on" -CONF_STATE_OFF = "state_off" +DEPENDENCIES = ['modbus'] REGISTER_TYPE_HOLDING = 'holding' REGISTER_TYPE_INPUT = 'input' REGISTERS_SCHEMA = vol.Schema({ - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_SLAVE): cv.positive_int, - vol.Required(CONF_REGISTER): cv.positive_int, - vol.Required(CONF_COMMAND_ON): cv.positive_int, vol.Required(CONF_COMMAND_OFF): cv.positive_int, - vol.Optional(CONF_VERIFY_STATE, default=True): cv.boolean, - vol.Optional(CONF_VERIFY_REGISTER): - cv.positive_int, + vol.Required(CONF_COMMAND_ON): cv.positive_int, + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_REGISTER): cv.positive_int, + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]), - vol.Optional(CONF_STATE_ON): cv.positive_int, + vol.Optional(CONF_SLAVE): cv.positive_int, vol.Optional(CONF_STATE_OFF): cv.positive_int, + vol.Optional(CONF_STATE_ON): cv.positive_int, + vol.Optional(CONF_VERIFY_REGISTER): cv.positive_int, + vol.Optional(CONF_VERIFY_STATE, default=True): cv.boolean, }) COILS_SCHEMA = vol.Schema({ - vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Required(CONF_COIL): cv.positive_int, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_SLAVE): cv.positive_int, + vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, }) PLATFORM_SCHEMA = vol.All( @@ -141,9 +141,9 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): """Representation of a Modbus register switch.""" # pylint: disable=super-init-not-called - def __init__(self, hub, name, slave, register, command_on, - command_off, verify_state, verify_register, - register_type, state_on, state_off): + def __init__(self, hub, name, slave, register, command_on, command_off, + verify_state, verify_register, register_type, state_on, + state_off): """Initialize the register switch.""" self._hub = hub self._name = name From 6e0186fd5656bd1e9f6e81de3895c0fd9ce4468c Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Sun, 24 Feb 2019 20:26:02 +1100 Subject: [PATCH 148/253] Bump nessclient version to 0.9.10 (#21388) * ness_alarm: Bump nessclient version * update requirements_all.txt --- homeassistant/components/ness_alarm/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index dae244ece3f..8dcb9541597 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -10,7 +10,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send -REQUIREMENTS = ['nessclient==0.9.9'] +REQUIREMENTS = ['nessclient==0.9.10'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a89deb3d1f3..42a5e792123 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -722,7 +722,7 @@ nanoleaf==0.4.1 ndms2_client==0.0.6 # homeassistant.components.ness_alarm -nessclient==0.9.9 +nessclient==0.9.10 # homeassistant.components.sensor.netdata netdata==0.1.2 From 3e9376c418ec0280355543b8184403d6cfe4dc08 Mon Sep 17 00:00:00 2001 From: koreth Date: Sun, 24 Feb 2019 04:56:52 -0800 Subject: [PATCH 149/253] Handle capitalized HomeKit property names (#21382) The Velux ACTIVE gateway uses `C#` rather than `c#` as the name of the property that holds the count of accessories. Apple's HomeKit docs suggest that properties should be case-insensitive, so update the code to not assume the property names are lower case. --- .../components/homekit_controller/__init__.py | 14 +++++++++++--- tests/components/homekit_controller/common.py | 13 ++++++++----- tests/components/homekit_controller/test_cover.py | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index ffeb9d625be..eb748a3d883 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -343,9 +343,17 @@ def setup(hass, config): # model, id host = discovery_info['host'] port = discovery_info['port'] - model = discovery_info['properties']['md'] - hkid = discovery_info['properties']['id'] - config_num = int(discovery_info['properties']['c#']) + + # Fold property keys to lower case, making them effectively + # case-insensitive. Some HomeKit devices capitalize them. + properties = { + key.lower(): value + for (key, value) in discovery_info['properties'].items() + } + + model = properties['md'] + hkid = properties['id'] + config_num = int(properties['c#']) if model in HOMEKIT_IGNORE: return diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index d3c1f9ab07b..d543cf51749 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -134,8 +134,11 @@ class FakeService(AbstractService): return char -async def setup_test_component(hass, services): - """Load a fake homekit accessory based on a homekit accessory model.""" +async def setup_test_component(hass, services, capitalize=False): + """Load a fake homekit accessory based on a homekit accessory model. + + If capitalize is True, property names will be in upper case. + """ domain = None for service in services: service_name = ServicesTypes.get_short(service.type) @@ -162,9 +165,9 @@ async def setup_test_component(hass, services): 'host': '127.0.0.1', 'port': 8080, 'properties': { - 'md': 'TestDevice', - 'id': '00:00:00:00:00:00', - 'c#': 1, + ('MD' if capitalize else 'md'): 'TestDevice', + ('ID' if capitalize else 'id'): '00:00:00:00:00:00', + ('C#' if capitalize else 'c#'): 1, } } diff --git a/tests/components/homekit_controller/test_cover.py b/tests/components/homekit_controller/test_cover.py index 062ecc54041..62fce4325c7 100644 --- a/tests/components/homekit_controller/test_cover.py +++ b/tests/components/homekit_controller/test_cover.py @@ -70,6 +70,20 @@ def create_window_covering_service_with_v_tilt(): return service +async def test_accept_capitalized_property_names(hass, utcnow): + """Test that we can handle a device with capitalized property names.""" + window_cover = create_window_covering_service() + helper = await setup_test_component(hass, [window_cover], capitalize=True) + + # The specific interaction we do here doesn't matter; we just need + # to do *something* to ensure that discovery properly dealt with the + # capitalized property names. + await hass.services.async_call('cover', 'open_cover', { + 'entity_id': helper.entity_id, + }, blocking=True) + assert helper.characteristics[POSITION_TARGET].value == 100 + + async def test_change_window_cover_state(hass, utcnow): """Test that we can turn a HomeKit alarm on and off again.""" window_cover = create_window_covering_service() From 65bc7a6fbd012a0b0b424f43f296a507d023e9e4 Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Sun, 24 Feb 2019 14:21:56 +0100 Subject: [PATCH 150/253] Update WazeTravelSensor to 0.9 (#21130) * Update WazeRouteCalculator to 0.8 and suppress logging Fixes #20071 and #21051 * Update requirements_all.txt * Update requirements_all.txt * Update waze_travel_time.py * Update waze_travel_time.py * Update waze_travel_time.py * Update waze_travel_time.py --- homeassistant/components/sensor/waze_travel_time.py | 5 +++-- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index 8ed4f50a9c7..0f8897de291 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -18,7 +18,7 @@ from homeassistant.helpers import location from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['WazeRouteCalculator==0.6'] +REQUIREMENTS = ['WazeRouteCalculator==0.9'] _LOGGER = logging.getLogger(__name__) @@ -204,7 +204,8 @@ class WazeTravelTime(Entity): if self._destination is not None and self._origin is not None: try: params = WazeRouteCalculator.WazeRouteCalculator( - self._origin, self._destination, self._region) + self._origin, self._destination, self._region, + log_lvl=logging.DEBUG) routes = params.calc_all_routes_info(real_time=self._realtime) if self._incl_filter is not None: diff --git a/requirements_all.txt b/requirements_all.txt index 42a5e792123..d3fae557be7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -78,7 +78,7 @@ TravisPy==0.3.5 TwitterAPI==2.5.9 # homeassistant.components.sensor.waze_travel_time -WazeRouteCalculator==0.6 +WazeRouteCalculator==0.9 # homeassistant.components.notify.yessssms YesssSMS==0.2.3 From 7255fbdf3ad96ecdfea94729e1b81afda7b96d31 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 24 Feb 2019 15:56:19 +0100 Subject: [PATCH 151/253] Clean up geofency test (#21397) --- tests/components/geofency/test_init.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 41c232a51c3..dd87a6d9503 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -10,11 +10,10 @@ from homeassistant.components.geofency import ( CONF_MOBILE_BEACONS, DOMAIN, TRACKER_UPDATE) from homeassistant.const import ( HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, STATE_HOME, - STATE_NOT_HOME, CONF_WEBHOOK_ID) + STATE_NOT_HOME) from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component from homeassistant.util import slugify -from tests.common import MockConfigEntry HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 @@ -288,16 +287,22 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id): @pytest.mark.xfail( reason='The device_tracker component does not support unloading yet.' ) -async def test_load_unload_entry(hass, geofency_client): +async def test_load_unload_entry(hass, geofency_client, webhook_id): """Test that the appropriate dispatch signals are added and removed.""" - entry = MockConfigEntry(domain=DOMAIN, data={ - CONF_WEBHOOK_ID: 'geofency_test' - }) + url = '/api/webhook/{}'.format(webhook_id) - await geofency.async_setup_entry(hass, entry) + # Enter the Home zone + req = await geofency_client.post(url, data=GPS_ENTER_HOME) await hass.async_block_till_done() - assert 1 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) + assert req.status == HTTP_OK + device_name = slugify(GPS_ENTER_HOME['device']) + state_name = hass.states.get('{}.{}'.format( + 'device_tracker', device_name)).state + assert STATE_HOME == state_name + assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1 - await geofency.async_unload_entry(hass, entry) + entry = hass.config_entries.async_entries(DOMAIN)[0] + + assert await geofency.async_unload_entry(hass, entry) await hass.async_block_till_done() - assert 0 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) + assert not hass.data[DATA_DISPATCHER][TRACKER_UPDATE] From 04fc951048762432df2b62230bfd94eb9d082c5f Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sun, 24 Feb 2019 10:45:11 -0600 Subject: [PATCH 152/253] Improve tolerance of SmartThings Climate platform (#21383) * Improve resilience of platform with buggy handlers * Eliminate possibility of None in operation list * Refactor variable name --- .../components/smartthings/climate.py | 45 ++++++++++++++++--- tests/components/smartthings/test_climate.py | 45 ++++++++++++++++++- 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index d1f58cf91f1..f660e905274 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -1,8 +1,10 @@ """Support for climate devices through the SmartThings cloud API.""" import asyncio -from typing import Optional, Sequence +import logging +from typing import Iterable, Optional, Sequence -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ( + DOMAIN as CLIMATE_DOMAIN, ClimateDevice) from homeassistant.components.climate.const import ( ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_FAN_MODE, @@ -38,6 +40,8 @@ UNIT_MAP = { 'F': TEMP_FAHRENHEIT } +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): @@ -50,7 +54,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] async_add_entities( [SmartThingsThermostat(device) for device in broker.devices.values() - if broker.any_assigned(device.device_id, 'climate')]) + if broker.any_assigned(device.device_id, CLIMATE_DOMAIN)], True) def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: @@ -90,6 +94,8 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): """Init the class.""" super().__init__(device) self._supported_features = self._determine_features() + self._current_operation = None + self._operations = None def _determine_features(self): from pysmartthings import Capability @@ -127,6 +133,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): if operation_state: mode = STATE_TO_MODE[operation_state] await self._device.set_thermostat_mode(mode, set_status=True) + await self.async_update() # Heat/cool setpoint heating_setpoint = None @@ -151,6 +158,33 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state(True) + async def async_update(self): + """Update the attributes of the climate device.""" + thermostat_mode = self._device.status.thermostat_mode + self._current_operation = MODE_TO_STATE.get(thermostat_mode) + if self._current_operation is None: + _LOGGER.debug('Device %s (%s) returned an invalid' + 'thermostat mode: %s', self._device.label, + self._device.device_id, thermostat_mode) + + supported_modes = self._device.status.supported_thermostat_modes + if isinstance(supported_modes, Iterable): + operations = set() + for mode in supported_modes: + state = MODE_TO_STATE.get(mode) + if state is not None: + operations.add(state) + else: + _LOGGER.debug('Device %s (%s) returned an invalid ' + 'supported thermostat mode: %s', + self._device.label, self._device.device_id, + mode) + self._operations = operations + else: + _LOGGER.debug('Device %s (%s) returned invalid supported ' + 'thermostat modes: %s', self._device.label, + self._device.device_id, supported_modes) + @property def current_fan_mode(self): """Return the fan setting.""" @@ -164,7 +198,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - return MODE_TO_STATE[self._device.status.thermostat_mode] + return self._current_operation @property def current_temperature(self): @@ -187,8 +221,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateDevice): @property def operation_list(self): """Return the list of available operation modes.""" - return {MODE_TO_STATE[mode] for mode in - self._device.status.supported_thermostat_modes} + return self._operations @property def supported_features(self): diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index d8b1346d225..481e43266fa 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -19,7 +19,8 @@ from homeassistant.components.climate.const import ( from homeassistant.components.smartthings import climate from homeassistant.components.smartthings.const import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, STATE_OFF) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, STATE_OFF, + STATE_UNKNOWN) from .conftest import setup_platform @@ -95,6 +96,25 @@ def thermostat_fixture(device_factory): return device +@pytest.fixture(name="buggy_thermostat") +def buggy_thermostat_fixture(device_factory): + """Fixture returns a buggy thermostat.""" + device = device_factory( + "Buggy Thermostat", + capabilities=[ + Capability.temperature_measurement, + Capability.thermostat_cooling_setpoint, + Capability.thermostat_heating_setpoint, + Capability.thermostat_mode], + status={ + Attribute.thermostat_mode: 'heating', + Attribute.cooling_setpoint: 74, + Attribute.heating_setpoint: 68} + ) + device.status.attributes[Attribute.temperature] = Status(70, 'F', None) + return device + + async def test_async_setup_platform(): """Test setup platform does nothing (it uses config entries).""" await climate.async_setup_platform(None, None, None) @@ -152,6 +172,29 @@ async def test_thermostat_entity_state(hass, thermostat): assert state.attributes[ATTR_CURRENT_HUMIDITY] == 34 +async def test_buggy_thermostat_entity_state(hass, buggy_thermostat): + """Tests the state attributes properly match the thermostat type.""" + await setup_platform(hass, CLIMATE_DOMAIN, buggy_thermostat) + state = hass.states.get('climate.buggy_thermostat') + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ + SUPPORT_OPERATION_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH | \ + SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_TARGET_TEMPERATURE + assert ATTR_OPERATION_LIST not in state.attributes + assert state.attributes[ATTR_TEMPERATURE] is None + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.1 # celsius + + +async def test_buggy_thermostat_invalid_mode(hass, buggy_thermostat): + """Tests when an invalid operation mode is included.""" + buggy_thermostat.status.update_attribute_value( + Attribute.supported_thermostat_modes, + ['heat', 'emergency heat', 'other']) + await setup_platform(hass, CLIMATE_DOMAIN, buggy_thermostat) + state = hass.states.get('climate.buggy_thermostat') + assert state.attributes[ATTR_OPERATION_LIST] == {'heat'} + + async def test_set_fan_mode(hass, thermostat): """Test the fan mode is set successfully.""" await setup_platform(hass, CLIMATE_DOMAIN, thermostat) From 47220d71a1984cb19dd4277bb23566ee01432449 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 24 Feb 2019 17:49:50 +0100 Subject: [PATCH 153/253] Clean up locative tests (#21400) --- tests/components/locative/test_init.py | 44 +++++++++++++++++--------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 877d25d04bd..f757080eadc 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -8,11 +8,11 @@ from homeassistant.components import locative from homeassistant.components.device_tracker import \ DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.locative import DOMAIN, TRACKER_UPDATE -from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, \ - CONF_WEBHOOK_ID +from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry + +# pylint: disable=redefined-outer-name @pytest.fixture(autouse=True) @@ -127,7 +127,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state - assert 'home' == state_name + assert state_name == 'home' data['id'] = 'HOME' data['trigger'] = 'exit' @@ -138,7 +138,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state - assert 'not_home' == state_name + assert state_name == 'not_home' data['id'] = 'hOmE' data['trigger'] = 'enter' @@ -149,7 +149,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state - assert 'home' == state_name + assert state_name == 'home' data['trigger'] = 'exit' @@ -159,7 +159,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state - assert 'not_home' == state_name + assert state_name == 'not_home' data['id'] = 'work' data['trigger'] = 'enter' @@ -170,7 +170,7 @@ async def test_enter_and_exit(hass, locative_client, webhook_id): assert req.status == HTTP_OK state_name = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, data['device'])).state - assert 'work' == state_name + assert state_name == 'work' async def test_exit_after_enter(hass, locative_client, webhook_id): @@ -243,16 +243,30 @@ async def test_exit_first(hass, locative_client, webhook_id): @pytest.mark.xfail( reason='The device_tracker component does not support unloading yet.' ) -async def test_load_unload_entry(hass): +async def test_load_unload_entry(hass, locative_client, webhook_id): """Test that the appropriate dispatch signals are added and removed.""" - entry = MockConfigEntry(domain=DOMAIN, data={ - CONF_WEBHOOK_ID: 'locative_test' - }) + url = '/api/webhook/{}'.format(webhook_id) - await locative.async_setup_entry(hass, entry) + data = { + 'latitude': 40.7855, + 'longitude': -111.7367, + 'device': 'new_device', + 'id': 'Home', + 'trigger': 'exit' + } + + # Exit Home + req = await locative_client.post(url, data=data) await hass.async_block_till_done() - assert 1 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) + assert req.status == HTTP_OK + + state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN, + data['device'])) + assert state.state == 'not_home' + assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1 + + entry = hass.config_entries.async_entries(DOMAIN)[0] await locative.async_unload_entry(hass, entry) await hass.async_block_till_done() - assert 0 == len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) + assert not hass.data[DATA_DISPATCHER][TRACKER_UPDATE] From a4bb35142c5d71db486da16423de744a85e896fa Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 24 Feb 2019 18:45:08 +0100 Subject: [PATCH 154/253] Add Sonos discovery of multiple households (#21337) * Remove confusing device naming * Add discovery of multiple households * Rename SonosDevice to SonosEntity --- homeassistant/components/sonos/__init__.py | 2 +- .../components/sonos/media_player.py | 85 +++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sonos/test_media_player.py | 86 +++++++++---------- 5 files changed, 89 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 69d5a9bfc33..bcac4ce272c 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -4,7 +4,7 @@ from homeassistant.helpers import config_entry_flow DOMAIN = 'sonos' -REQUIREMENTS = ['pysonos==0.0.6'] +REQUIREMENTS = ['pysonos==0.0.7'] async def async_setup(hass, config): diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 2a7eafaf835..ee2f9e6b4dc 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -48,7 +48,7 @@ SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer' SERVICE_UPDATE_ALARM = 'sonos_update_alarm' SERVICE_SET_OPTION = 'sonos_set_option' -DATA_SONOS = 'sonos_devices' +DATA_SONOS = 'sonos_media_player' SOURCE_LINEIN = 'Line-in' SOURCE_TV = 'TV' @@ -114,7 +114,7 @@ class SonosData: def __init__(self): """Initialize the data.""" self.uids = set() - self.devices = [] + self.entities = [] self.topology_lock = threading.Lock() @@ -129,9 +129,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" - def add_entities(devices, update_before_add=False): - """Sync version of async add devices.""" - hass.add_job(async_add_entities, devices, update_before_add) + def add_entities(entities, update_before_add=False): + """Sync version of async add entities.""" + hass.add_job(async_add_entities, entities, update_before_add) hass.async_add_executor_job( _setup_platform, hass, hass.data[SONOS_DOMAIN].get('media_player', {}), @@ -153,7 +153,7 @@ def _setup_platform(hass, config, add_entities, discovery_info): if discovery_info: player = pysonos.SoCo(discovery_info.get('host')) - # If device already exists by config + # If host already exists by config if player.uid in hass.data[DATA_SONOS].uids: return @@ -176,53 +176,54 @@ def _setup_platform(hass, config, add_entities, discovery_info): _LOGGER.warning("Failed to initialize '%s'", host) else: players = pysonos.discover( - interface_addr=config.get(CONF_INTERFACE_ADDR)) + interface_addr=config.get(CONF_INTERFACE_ADDR), + all_households=True) if not players: _LOGGER.warning("No Sonos speakers found") return hass.data[DATA_SONOS].uids.update(p.uid for p in players) - add_entities(SonosDevice(p) for p in players) + add_entities(SonosEntity(p) for p in players) _LOGGER.debug("Added %s Sonos speakers", len(players)) def service_handle(service): """Handle for services.""" entity_ids = service.data.get('entity_id') - devices = hass.data[DATA_SONOS].devices + entities = hass.data[DATA_SONOS].entities if entity_ids: - devices = [d for d in devices if d.entity_id in entity_ids] + entities = [e for e in entities if e.entity_id in entity_ids] if service.service == SERVICE_JOIN: - master = [device for device in hass.data[DATA_SONOS].devices - if device.entity_id == service.data[ATTR_MASTER]] + master = [e for e in hass.data[DATA_SONOS].entities + if e.entity_id == service.data[ATTR_MASTER]] if master: with hass.data[DATA_SONOS].topology_lock: - master[0].join(devices) + master[0].join(entities) return if service.service == SERVICE_UNJOIN: with hass.data[DATA_SONOS].topology_lock: - for device in devices: - device.unjoin() + for entity in entities: + entity.unjoin() return - for device in devices: + for entity in entities: if service.service == SERVICE_SNAPSHOT: - device.snapshot(service.data[ATTR_WITH_GROUP]) + entity.snapshot(service.data[ATTR_WITH_GROUP]) elif service.service == SERVICE_RESTORE: - device.restore(service.data[ATTR_WITH_GROUP]) + entity.restore(service.data[ATTR_WITH_GROUP]) elif service.service == SERVICE_SET_TIMER: - device.set_sleep_timer(service.data[ATTR_SLEEP_TIME]) + entity.set_sleep_timer(service.data[ATTR_SLEEP_TIME]) elif service.service == SERVICE_CLEAR_TIMER: - device.clear_sleep_timer() + entity.clear_sleep_timer() elif service.service == SERVICE_UPDATE_ALARM: - device.set_alarm(**service.data) + entity.set_alarm(**service.data) elif service.service == SERVICE_SET_OPTION: - device.set_option(**service.data) + entity.set_option(**service.data) - device.schedule_update_ha_state(True) + entity.schedule_update_ha_state(True) hass.services.register( DOMAIN, SERVICE_JOIN, service_handle, @@ -270,9 +271,9 @@ class _ProcessSonosEventQueue: def _get_entity_from_soco_uid(hass, uid): - """Return SonosDevice from SoCo uid.""" - for entity in hass.data[DATA_SONOS].devices: - if uid == entity.soco.uid: + """Return SonosEntity from SoCo uid.""" + for entity in hass.data[DATA_SONOS].entities: + if uid == entity.unique_id: return entity return None @@ -303,11 +304,11 @@ def soco_error(errorcodes=None): def soco_coordinator(funct): """Call function on coordinator.""" @ft.wraps(funct) - def wrapper(device, *args, **kwargs): + def wrapper(entity, *args, **kwargs): """Wrap for call to coordinator.""" - if device.is_coordinator: - return funct(device, *args, **kwargs) - return funct(device.coordinator, *args, **kwargs) + if entity.is_coordinator: + return funct(entity, *args, **kwargs) + return funct(entity.coordinator, *args, **kwargs) return wrapper @@ -329,11 +330,11 @@ def _is_radio_uri(uri): return uri.startswith(radio_schemes) -class SonosDevice(MediaPlayerDevice): - """Representation of a Sonos device.""" +class SonosEntity(MediaPlayerDevice): + """Representation of a Sonos entity.""" def __init__(self, player): - """Initialize the Sonos device.""" + """Initialize the Sonos entity.""" self._subscriptions = [] self._receives_events = False self._volume_increment = 2 @@ -366,7 +367,7 @@ class SonosDevice(MediaPlayerDevice): async def async_added_to_hass(self): """Subscribe sonos events.""" - self.hass.data[DATA_SONOS].devices.append(self) + self.hass.data[DATA_SONOS].entities.append(self) self.hass.async_add_executor_job(self._subscribe_to_player_events) @property @@ -376,7 +377,7 @@ class SonosDevice(MediaPlayerDevice): @property def name(self): - """Return the name of the device.""" + """Return the name of the entity.""" return self._name @property @@ -394,7 +395,7 @@ class SonosDevice(MediaPlayerDevice): @property @soco_coordinator def state(self): - """Return the state of the device.""" + """Return the state of the entity.""" if self._status in ('PAUSED_PLAYBACK', 'STOPPED'): return STATE_PAUSED if self._status in ('PLAYING', 'TRANSITIONING'): @@ -410,7 +411,7 @@ class SonosDevice(MediaPlayerDevice): @property def soco(self): - """Return soco device.""" + """Return soco object.""" return self._player @property @@ -434,7 +435,7 @@ class SonosDevice(MediaPlayerDevice): return False def _set_basic_information(self): - """Set initial device information.""" + """Set initial entity information.""" speaker_info = self.soco.get_speaker_info(True) self._name = speaker_info['zone_name'] self._model = speaker_info['model_name'] @@ -477,8 +478,8 @@ class SonosDevice(MediaPlayerDevice): self._receives_events = False # New player available, build the current group topology - for device in self.hass.data[DATA_SONOS].devices: - device.update_groups() + for entity in self.hass.data[DATA_SONOS].entities: + entity.update_groups() player = self.soco @@ -554,7 +555,7 @@ class SonosDevice(MediaPlayerDevice): self.schedule_update_ha_state() # Also update slaves - for entity in self.hass.data[DATA_SONOS].devices: + for entity in self.hass.data[DATA_SONOS].entities: coordinator = entity.coordinator if coordinator and coordinator.unique_id == self.unique_id: entity.schedule_update_ha_state() @@ -1087,7 +1088,7 @@ class SonosDevice(MediaPlayerDevice): @property def device_state_attributes(self): - """Return device specific state attributes.""" + """Return entity specific state attributes.""" attributes = {ATTR_SONOS_GROUP: self._sonos_group} if self._night_sound is not None: diff --git a/requirements_all.txt b/requirements_all.txt index d3fae557be7..d885b3004ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1263,7 +1263,7 @@ pysmartthings==0.6.3 pysnmp==4.4.8 # homeassistant.components.sonos -pysonos==0.0.6 +pysonos==0.0.7 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64c28534046..f3b8e6ac03e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -226,7 +226,7 @@ pysmartapp==0.3.0 pysmartthings==0.6.3 # homeassistant.components.sonos -pysonos==0.0.6 +pysonos==0.0.7 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 55ff96f202a..d57c730a9f8 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -21,7 +21,7 @@ ENTITY_ID = 'media_player.kitchen' class pysonosDiscoverMock(): """Mock class for the pysonos.discover method.""" - def discover(interface_addr): + def discover(interface_addr, all_households=False): """Return tuple of pysonos.SoCo objects representing found speakers.""" return {SoCoMock('192.0.2.1')} @@ -123,10 +123,10 @@ class SoCoMock(): def add_entities_factory(hass): - """Add devices factory.""" - def add_entities(devices, update_befor_add=False): - """Fake add device.""" - hass.data[sonos.DATA_SONOS].devices = devices + """Add entities factory.""" + def add_entities(entities, update_befor_add=False): + """Fake add entity.""" + hass.data[sonos.DATA_SONOS].entities = entities return add_entities @@ -144,14 +144,14 @@ class TestSonosMediaPlayer(unittest.TestCase): return True # Monkey patches - self.real_available = sonos.SonosDevice.available - sonos.SonosDevice.available = monkey_available + self.real_available = sonos.SonosEntity.available + sonos.SonosEntity.available = monkey_available # pylint: disable=invalid-name def tearDown(self): """Stop everything that was started.""" # Monkey patches - sonos.SonosDevice.available = self.real_available + sonos.SonosEntity.available = self.real_available self.hass.stop() @mock.patch('pysonos.SoCo', new=SoCoMock) @@ -162,9 +162,9 @@ class TestSonosMediaPlayer(unittest.TestCase): 'host': '192.0.2.1' }) - devices = list(self.hass.data[sonos.DATA_SONOS].devices) - assert len(devices) == 1 - assert devices[0].name == 'Kitchen' + entities = list(self.hass.data[sonos.DATA_SONOS].entities) + assert len(entities) == 1 + assert entities[0].name == 'Kitchen' @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) @@ -182,7 +182,7 @@ class TestSonosMediaPlayer(unittest.TestCase): assert setup_component(self.hass, DOMAIN, config) - assert len(self.hass.data[sonos.DATA_SONOS].devices) == 1 + assert len(self.hass.data[sonos.DATA_SONOS].entities) == 1 assert discover_mock.call_count == 1 @mock.patch('pysonos.SoCo', new=SoCoMock) @@ -198,9 +198,9 @@ class TestSonosMediaPlayer(unittest.TestCase): assert setup_component(self.hass, DOMAIN, config) - devices = self.hass.data[sonos.DATA_SONOS].devices - assert len(devices) == 1 - assert devices[0].name == 'Kitchen' + entities = self.hass.data[sonos.DATA_SONOS].entities + assert len(entities) == 1 + assert entities[0].name == 'Kitchen' @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) @@ -215,9 +215,9 @@ class TestSonosMediaPlayer(unittest.TestCase): assert setup_component(self.hass, DOMAIN, config) - devices = self.hass.data[sonos.DATA_SONOS].devices - assert len(devices) == 2 - assert devices[0].name == 'Kitchen' + entities = self.hass.data[sonos.DATA_SONOS].entities + assert len(entities) == 2 + assert entities[0].name == 'Kitchen' @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) @@ -232,9 +232,9 @@ class TestSonosMediaPlayer(unittest.TestCase): assert setup_component(self.hass, DOMAIN, config) - devices = self.hass.data[sonos.DATA_SONOS].devices - assert len(devices) == 2 - assert devices[0].name == 'Kitchen' + entities = self.hass.data[sonos.DATA_SONOS].entities + assert len(entities) == 2 + assert entities[0].name == 'Kitchen' @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch.object(pysonos, 'discover', new=pysonosDiscoverMock.discover) @@ -242,9 +242,9 @@ class TestSonosMediaPlayer(unittest.TestCase): def test_ensure_setup_sonos_discovery(self, *args): """Test a single device using the autodiscovery provided by Sonos.""" sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass)) - devices = list(self.hass.data[sonos.DATA_SONOS].devices) - assert len(devices) == 1 - assert devices[0].name == 'Kitchen' + entities = list(self.hass.data[sonos.DATA_SONOS].entities) + assert len(entities) == 1 + assert entities[0].name == 'Kitchen' @mock.patch('pysonos.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) @@ -254,10 +254,10 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] - device.hass = self.hass + entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1] + entity.hass = self.hass - device.set_sleep_timer(30) + entity.set_sleep_timer(30) set_sleep_timerMock.assert_called_once_with(30) @mock.patch('pysonos.SoCo', new=SoCoMock) @@ -268,10 +268,10 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] - device.hass = self.hass + entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1] + entity.hass = self.hass - device.set_sleep_timer(None) + entity.set_sleep_timer(None) set_sleep_timerMock.assert_called_once_with(None) @mock.patch('pysonos.SoCo', new=SoCoMock) @@ -282,8 +282,8 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] - device.hass = self.hass + entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1] + entity.hass = self.hass alarm1 = alarms.Alarm(pysonos_mock) alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False, include_linked_zones=False, volume=100) @@ -294,9 +294,9 @@ class TestSonosMediaPlayer(unittest.TestCase): 'include_linked_zones': True, 'volume': 0.30, } - device.set_alarm(alarm_id=2) + entity.set_alarm(alarm_id=2) alarm1.save.assert_not_called() - device.set_alarm(alarm_id=1, **attrs) + entity.set_alarm(alarm_id=1, **attrs) assert alarm1.enabled == attrs['enabled'] assert alarm1.start_time == attrs['time'] assert alarm1.include_linked_zones == \ @@ -312,11 +312,11 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] - device.hass = self.hass + entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1] + entity.hass = self.hass snapshotMock.return_value = True - device.snapshot() + entity.snapshot() assert snapshotMock.call_count == 1 assert snapshotMock.call_args == mock.call() @@ -330,13 +330,13 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] - device.hass = self.hass + entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1] + entity.hass = self.hass restoreMock.return_value = True - device._snapshot_coordinator = mock.MagicMock() - device._snapshot_coordinator.soco_device = SoCoMock('192.0.2.17') - device._soco_snapshot = Snapshot(device._player) - device.restore() + entity._snapshot_coordinator = mock.MagicMock() + entity._snapshot_coordinator.soco_entity = SoCoMock('192.0.2.17') + entity._soco_snapshot = Snapshot(entity._player) + entity.restore() assert restoreMock.call_count == 1 assert restoreMock.call_args == mock.call(False) From 814e610b1deda1e331c85e41b5d12b991f1bd18a Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 24 Feb 2019 21:33:07 +0100 Subject: [PATCH 155/253] Philips js state (#21407) * Switch to SCAN_INTERVAL instead of throttle This allows forced update of state * Don't change tv on/off state in services * Drop unused variables * Only send mute if different from current state * No need to update variables, will behandled on update * Drop unused import --- .../components/media_player/philips_js.py | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index 4f8a1339781..97ec758e6cf 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -19,13 +19,12 @@ from homeassistant.const import ( CONF_API_VERSION, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script -from homeassistant.util import Throttle REQUIREMENTS = ['ha-philipsjs==0.0.5'] _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(seconds=30) SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -72,8 +71,6 @@ class PhilipsTV(MediaPlayerDevice): self._tv = tv self._name = name self._state = None - self._min_volume = None - self._max_volume = None self._volume = None self._muted = False self._program_name = None @@ -123,10 +120,6 @@ class PhilipsTV(MediaPlayerDevice): """Set the input source.""" if source in self._source_mapping: self._tv.setSource(self._source_mapping.get(source)) - self._source = source - if not self._tv.on: - self._state = STATE_OFF - self._watching_tv = bool(self._tv.source_id == 'tv') @property def volume_level(self): @@ -146,26 +139,20 @@ class PhilipsTV(MediaPlayerDevice): def turn_off(self): """Turn off the device.""" self._tv.sendKey('Standby') - if not self._tv.on: - self._state = STATE_OFF def volume_up(self): """Send volume up command.""" self._tv.sendKey('VolumeUp') - if not self._tv.on: - self._state = STATE_OFF def volume_down(self): """Send volume down command.""" self._tv.sendKey('VolumeDown') - if not self._tv.on: - self._state = STATE_OFF def mute_volume(self, mute): """Send mute command.""" - self._tv.sendKey('Mute') - if not self._tv.on: - self._state = STATE_OFF + if self._muted != mute: + self._tv.sendKey('Mute') + self._muted = mute def set_volume_level(self, volume): """Set volume level, range 0..1.""" @@ -186,12 +173,9 @@ class PhilipsTV(MediaPlayerDevice): return '{} - {}'.format(self._source, self._channel_name) return self._source - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data and update device state.""" self._tv.update() - self._min_volume = self._tv.min_volume - self._max_volume = self._tv.max_volume self._volume = self._tv.volume self._muted = self._tv.muted if self._tv.source_id: From ff93cdb0bc1c0f90fe93dce6203aece4e1a3a723 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 24 Feb 2019 15:16:49 -0800 Subject: [PATCH 156/253] Add ADB server functionality to Fire TV (#21221) * Bump firetv to 1.0.8 * Update the 'update' function for Fire TV * Return None for properties when unavailable * Remove 'self.adb_lock' attribute * Remove threading import * Update configuration for Fire TV component * Clarify 'python-adb' vs. 'pure-python-adb' * Rename '__adb_decorator' to '_adb_exception_catcher' * Don't check 'self._available' in properties * Bump firetv to 1.0.9 --- .../components/media_player/firetv.py | 163 +++++++----------- requirements_all.txt | 2 +- 2 files changed, 61 insertions(+), 104 deletions(-) diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py index 58f1913b9f9..fb7df736e51 100644 --- a/homeassistant/components/media_player/firetv.py +++ b/homeassistant/components/media_player/firetv.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/media_player.firetv/ """ import functools import logging -import threading import voluptuous as vol from homeassistant.components.media_player import ( @@ -20,22 +19,22 @@ from homeassistant.const import ( STATE_PLAYING, STATE_STANDBY) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['firetv==1.0.7'] +REQUIREMENTS = ['firetv==1.0.9'] _LOGGER = logging.getLogger(__name__) -SUPPORT_FIRETV = SUPPORT_PAUSE | \ +SUPPORT_FIRETV = SUPPORT_PAUSE | SUPPORT_PLAY | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ - SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_STOP | \ - SUPPORT_PLAY + SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_STOP CONF_ADBKEY = 'adbkey' -CONF_GET_SOURCE = 'get_source' +CONF_ADB_SERVER_IP = 'adb_server_ip' +CONF_ADB_SERVER_PORT = 'adb_server_port' CONF_GET_SOURCES = 'get_sources' DEFAULT_NAME = 'Amazon Fire TV' DEFAULT_PORT = 5555 -DEFAULT_GET_SOURCE = True +DEFAULT_ADB_SERVER_PORT = 5037 DEFAULT_GET_SOURCES = True @@ -52,12 +51,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_ADBKEY): has_adb_files, - vol.Optional(CONF_GET_SOURCE, default=DEFAULT_GET_SOURCE): cv.boolean, + vol.Optional(CONF_ADB_SERVER_IP): cv.string, + vol.Optional( + CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): cv.port, vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean }) -PACKAGE_LAUNCHER = "com.amazon.tv.launcher" -PACKAGE_SETTINGS = "com.amazon.tv.settings" +# Translate from `FireTV` reported state to HA state. +FIRETV_STATES = {'off': STATE_OFF, + 'idle': STATE_IDLE, + 'standby': STATE_STANDBY, + 'playing': STATE_PLAYING, + 'paused': STATE_PAUSED} def setup_platform(hass, config, add_entities, discovery_info=None): @@ -66,82 +71,82 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = '{0}:{1}'.format(config[CONF_HOST], config[CONF_PORT]) - if CONF_ADBKEY in config: - ftv = FireTV(host, config[CONF_ADBKEY]) - adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY]) + if CONF_ADB_SERVER_IP not in config: + # Use "python-adb" (Python ADB implementation) + if CONF_ADBKEY in config: + ftv = FireTV(host, config[CONF_ADBKEY]) + adb_log = " using adbkey='{0}'".format(config[CONF_ADBKEY]) + else: + ftv = FireTV(host) + adb_log = "" else: - ftv = FireTV(host) - adb_log = "" + # Use "pure-python-adb" (communicate with ADB server) + ftv = FireTV(host, adb_server_ip=config[CONF_ADB_SERVER_IP], + adb_server_port=config[CONF_ADB_SERVER_PORT]) + adb_log = " using ADB server at {0}:{1}".format( + config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT]) if not ftv.available: _LOGGER.warning("Could not connect to Fire TV at %s%s", host, adb_log) return name = config[CONF_NAME] - get_source = config[CONF_GET_SOURCE] get_sources = config[CONF_GET_SOURCES] - device = FireTVDevice(ftv, name, get_source, get_sources) + device = FireTVDevice(ftv, name, get_sources) add_entities([device]) _LOGGER.debug("Setup Fire TV at %s%s", host, adb_log) def adb_decorator(override_available=False): - """Send an ADB command if the device is available and not locked.""" - def adb_wrapper(func): + """Send an ADB command if the device is available and catch exceptions.""" + def _adb_decorator(func): """Wait if previous ADB commands haven't finished.""" @functools.wraps(func) - def _adb_wrapper(self, *args, **kwargs): + def _adb_exception_catcher(self, *args, **kwargs): # If the device is unavailable, don't do anything if not self.available and not override_available: return None - # If an ADB command is already running, skip this command - if not self.adb_lock.acquire(blocking=False): - _LOGGER.info("Skipping an ADB command because a previous " - "command is still running") - return None - - # Additional ADB commands will be prevented while trying this one try: - returns = func(self, *args, **kwargs) + return func(self, *args, **kwargs) except self.exceptions as err: _LOGGER.error( "Failed to execute an ADB command. ADB connection re-" "establishing attempt in the next update. Error: %s", err) - returns = None self._available = False # pylint: disable=protected-access - finally: - self.adb_lock.release() + return None - return returns + return _adb_exception_catcher - return _adb_wrapper - - return adb_wrapper + return _adb_decorator class FireTVDevice(MediaPlayerDevice): """Representation of an Amazon Fire TV device on the network.""" - def __init__(self, ftv, name, get_source, get_sources): + def __init__(self, ftv, name, get_sources): """Initialize the FireTV device.""" - from adb.adb_protocol import ( - InvalidChecksumError, InvalidCommandError, InvalidResponseError) - self.firetv = ftv self._name = name - self._get_source = get_source self._get_sources = get_sources - # whether or not the ADB connection is currently in use - self.adb_lock = threading.Lock() - # ADB exceptions to catch - self.exceptions = ( - AttributeError, BrokenPipeError, TypeError, ValueError, - InvalidChecksumError, InvalidCommandError, InvalidResponseError) + if not self.firetv.adb_server_ip: + # Using "python-adb" (Python ADB implementation) + from adb.adb_protocol import (InvalidChecksumError, + InvalidCommandError, + InvalidResponseError) + from adb.usb_exceptions import TcpTimeoutException + + self.exceptions = (AttributeError, BrokenPipeError, TypeError, + ValueError, InvalidChecksumError, + InvalidCommandError, InvalidResponseError, + TcpTimeoutException) + else: + # Using "pure-python-adb" (communicate with ADB server) + self.exceptions = (ConnectionResetError,) self._state = None self._available = self.firetv.available @@ -190,72 +195,24 @@ class FireTVDevice(MediaPlayerDevice): @adb_decorator(override_available=True) def update(self): - """Get the latest date and update device state.""" + """Update the device state and, if necessary, re-connect.""" # Check if device is disconnected. if not self._available: - self._running_apps = None - self._current_app = None - # Try to connect - self.firetv.connect() - self._available = self.firetv.available + self._available = self.firetv.connect() + + # To be safe, wait until the next update to run ADB commands. + return # If the ADB connection is not intact, don't update. if not self._available: return - # Check if device is off. - if not self.firetv.screen_on: - self._state = STATE_OFF - self._running_apps = None - self._current_app = None + # Get the `state`, `current_app`, and `running_apps`. + ftv_state, self._current_app, self._running_apps = \ + self.firetv.update(self._get_sources) - # Check if screen saver is on. - elif not self.firetv.awake: - self._state = STATE_IDLE - self._running_apps = None - self._current_app = None - - else: - # Get the running apps. - if self._get_sources: - self._running_apps = self.firetv.running_apps - - # Get the current app. - if self._get_source: - current_app = self.firetv.current_app - if isinstance(current_app, dict)\ - and 'package' in current_app: - self._current_app = current_app['package'] - else: - self._current_app = current_app - - # Show the current app as the only running app. - if not self._get_sources: - if self._current_app: - self._running_apps = [self._current_app] - else: - self._running_apps = None - - # Check if the launcher is active. - if self._current_app in [PACKAGE_LAUNCHER, PACKAGE_SETTINGS]: - self._state = STATE_STANDBY - - # Check for a wake lock (device is playing). - elif self.firetv.wake_lock: - self._state = STATE_PLAYING - - # Otherwise, device is paused. - else: - self._state = STATE_PAUSED - - # Don't get the current app. - elif self.firetv.wake_lock: - # Check for a wake lock (device is playing). - self._state = STATE_PLAYING - else: - # Assume the devices is on standby. - self._state = STATE_STANDBY + self._state = FIRETV_STATES[ftv_state] @adb_decorator() def turn_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index d885b3004ca..2b85c2d1fa5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -420,7 +420,7 @@ fiblary3==0.1.7 fints==1.0.1 # homeassistant.components.media_player.firetv -firetv==1.0.7 +firetv==1.0.9 # homeassistant.components.sensor.fitbit fitbit==0.3.0 From dd5fc0a1dab933267a9634b4d6cb8c84d9204425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 25 Feb 2019 07:36:57 +0200 Subject: [PATCH 157/253] Upgrade pytest to 4.3.0 (#21412) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 8e647010426..15f9b174504 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.6.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==4.2.1 +pytest==4.3.0 requests_mock==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3b8e6ac03e..0c1bec569d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,7 +14,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.6.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==4.2.1 +pytest==4.3.0 requests_mock==1.5.2 From 619ea3ff98f755c4ea9fcad2fb092c19ae1ccc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 25 Feb 2019 07:54:54 +0200 Subject: [PATCH 158/253] Upgrade mypy to 0.670 (#20934) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 15f9b174504..1a862f2d406 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ coveralls==1.2.0 flake8-docstrings==1.3.0 flake8==3.7.5 mock-open==1.3.1 -mypy==0.660 +mypy==0.670 pydocstyle==3.0.0 pylint==2.2.2 pytest-aiohttp==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c1bec569d4..ba283d1d2c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ coveralls==1.2.0 flake8-docstrings==1.3.0 flake8==3.7.5 mock-open==1.3.1 -mypy==0.660 +mypy==0.670 pydocstyle==3.0.0 pylint==2.2.2 pytest-aiohttp==0.3.0 From f190b698c6b6775ad21a6457255d0b7ee3344ee9 Mon Sep 17 00:00:00 2001 From: Baptiste Lecocq Date: Mon, 25 Feb 2019 07:44:33 +0100 Subject: [PATCH 159/253] Update pylinky (#21416) --- homeassistant/components/sensor/linky.py | 3 ++- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/linky.py b/homeassistant/components/sensor/linky.py index 316da010ae4..8130961bfc0 100644 --- a/homeassistant/components/sensor/linky.py +++ b/homeassistant/components/sensor/linky.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pylinky==0.1.8'] +REQUIREMENTS = ['pylinky==0.3.0'] _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=10) @@ -38,6 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): from pylinky.client import LinkyClient, PyLinkyError client = LinkyClient(username, password, None, timeout) try: + client.login() client.fetch_data() except PyLinkyError as exp: _LOGGER.error(exp) diff --git a/requirements_all.txt b/requirements_all.txt index 2b85c2d1fa5..1e9595418ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1113,7 +1113,7 @@ pylgnetcast-homeassistant==0.2.0.dev0 pylgtv==0.1.9 # homeassistant.components.sensor.linky -pylinky==0.1.8 +pylinky==0.3.0 # homeassistant.components.litejet pylitejet==0.1 From a50bcdff1a35279ebad3e468d0d8e0fa697974b8 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 25 Feb 2019 07:45:40 +0100 Subject: [PATCH 160/253] Mark water_heater as significant domain (#21390) --- homeassistant/components/history/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index fb037e8909d..7b07fac19a6 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -29,7 +29,7 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) -SIGNIFICANT_DOMAINS = ('thermostat', 'climate') +SIGNIFICANT_DOMAINS = ('thermostat', 'climate', 'water_heater') IGNORE_DOMAINS = ('zone', 'scene',) From 0ccbf61aeaa76aacaa75d1bd0e2f5a908e297fb9 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Mon, 25 Feb 2019 08:50:16 -0600 Subject: [PATCH 161/253] Add power and energy attributes to SmartThings switch (#21375) --- homeassistant/components/smartthings/switch.py | 16 +++++++++++++++- tests/components/smartthings/test_switch.py | 14 +++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index 5a1224f4fc2..d30aa3a2303 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -29,7 +29,9 @@ def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: # Must be able to be turned on/off. if Capability.switch in capabilities: - return [Capability.switch] + return [Capability.switch, + Capability.energy_meter, + Capability.power_meter] return None @@ -50,6 +52,18 @@ class SmartThingsSwitch(SmartThingsEntity, SwitchDevice): # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state() + @property + def current_power_w(self): + """Return the current power usage in W.""" + from pysmartthings import Attribute + return self._device.status.attributes[Attribute.power].value + + @property + def today_energy_kwh(self): + """Return the today total energy usage in kWh.""" + from pysmartthings import Attribute + return self._device.status.attributes[Attribute.energy].value + @property def is_on(self) -> bool: """Return true if light is on.""" diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 6ad87b7ad53..7d21db00460 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -9,7 +9,8 @@ from pysmartthings import Attribute, Capability from homeassistant.components.smartthings import switch from homeassistant.components.smartthings.const import ( DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch import ( + ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH, DOMAIN as SWITCH_DOMAIN) from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform @@ -61,8 +62,13 @@ async def test_turn_off(hass, device_factory): async def test_turn_on(hass, device_factory): """Test the switch turns of successfully.""" # Arrange - device = device_factory('Switch_1', [Capability.switch], - {Attribute.switch: 'off'}) + device = device_factory('Switch_1', + [Capability.switch, + Capability.power_meter, + Capability.energy_meter], + {Attribute.switch: 'off', + Attribute.power: 355, + Attribute.energy: 11.422}) await setup_platform(hass, SWITCH_DOMAIN, device) # Act await hass.services.async_call( @@ -72,6 +78,8 @@ async def test_turn_on(hass, device_factory): state = hass.states.get('switch.switch_1') assert state is not None assert state.state == 'on' + assert state.attributes[ATTR_CURRENT_POWER_W] == 355 + assert state.attributes[ATTR_TODAY_ENERGY_KWH] == 11.422 async def test_update_from_signal(hass, device_factory): From d3f1ee4a89f87cc2fe68e7c4e581bce2711013df Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Mon, 25 Feb 2019 12:13:34 -0600 Subject: [PATCH 162/253] Add SmartThings Cover platform and add cover device classes (#21192) * Add additional device classes to Cover component; Add SmartThings cover platform; Improve lock test coverage * Enhance cover platform to support position and battery level reporting. * Add additional classes * Removed device class descriptions * Updates based on review feedback * Add test case for closed --- homeassistant/components/cover/__init__.py | 23 +- homeassistant/components/smartthings/const.py | 3 +- homeassistant/components/smartthings/cover.py | 153 ++++++++++++++ .../components/smartthings/sensor.py | 8 +- tests/components/smartthings/test_cover.py | 196 ++++++++++++++++++ 5 files changed, 371 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/smartthings/cover.py create mode 100644 tests/components/smartthings/test_cover.py diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index bd003f1ad67..8b4031f09ed 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -35,12 +35,27 @@ ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers') ENTITY_ID_FORMAT = DOMAIN + '.{}' +# Refer to the cover dev docs for device class descriptions +DEVICE_CLASS_AWNING = 'awning' +DEVICE_CLASS_BLIND = 'blind' +DEVICE_CLASS_CURTAIN = 'curtain' +DEVICE_CLASS_DAMPER = 'damper' +DEVICE_CLASS_DOOR = 'door' +DEVICE_CLASS_GARAGE = 'garage' +DEVICE_CLASS_SHADE = 'shade' +DEVICE_CLASS_SHUTTER = 'shutter' +DEVICE_CLASS_WINDOW = 'window' DEVICE_CLASSES = [ - 'damper', - 'garage', # Garage door control - 'window', # Window control + DEVICE_CLASS_AWNING, + DEVICE_CLASS_BLIND, + DEVICE_CLASS_CURTAIN, + DEVICE_CLASS_DAMPER, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_SHADE, + DEVICE_CLASS_SHUTTER, + DEVICE_CLASS_WINDOW ] - DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) SUPPORT_OPEN = 1 diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index d423bcde44f..5da43203e4f 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -25,12 +25,13 @@ SETTINGS_INSTANCE_ID = "hassInstanceId" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 # Ordered 'specific to least-specific platform' in order for capabilities -# to be drawn-down and represented by the appropriate platform. +# to be drawn-down and represented by the most appropriate platform. SUPPORTED_PLATFORMS = [ 'climate', 'fan', 'light', 'lock', + 'cover', 'switch', 'binary_sensor', 'sensor' diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py new file mode 100644 index 00000000000..131da75f4fe --- /dev/null +++ b/homeassistant/components/smartthings/cover.py @@ -0,0 +1,153 @@ +"""Support for covers through the SmartThings cloud API.""" +from typing import Optional, Sequence + +from homeassistant.components.cover import ( + ATTR_POSITION, DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE, DEVICE_CLASS_SHADE, + DOMAIN as COVER_DOMAIN, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, + STATE_OPENING, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + CoverDevice) +from homeassistant.const import ATTR_BATTERY_LEVEL + +from . import SmartThingsEntity +from .const import DATA_BROKERS, DOMAIN + +DEPENDENCIES = ['smartthings'] + +VALUE_TO_STATE = { + 'closed': STATE_CLOSED, + 'closing': STATE_CLOSING, + 'open': STATE_OPEN, + 'opening': STATE_OPENING, + 'partially open': STATE_OPEN, + 'unknown': None +} + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add covers for a config entry.""" + broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] + async_add_entities( + [SmartThingsCover(device) for device in broker.devices.values() + if broker.any_assigned(device.device_id, COVER_DOMAIN)], True) + + +def get_capabilities(capabilities: Sequence[str]) -> Optional[Sequence[str]]: + """Return all capabilities supported if minimum required are present.""" + from pysmartthings import Capability + + min_required = [ + Capability.door_control, + Capability.garage_door_control, + Capability.window_shade + ] + # Must have one of the min_required + if any(capability in capabilities + for capability in min_required): + # Return all capabilities supported/consumed + return min_required + [Capability.battery, Capability.switch_level] + + return None + + +class SmartThingsCover(SmartThingsEntity, CoverDevice): + """Define a SmartThings cover.""" + + def __init__(self, device): + """Initialize the cover class.""" + from pysmartthings import Capability + + super().__init__(device) + self._device_class = None + self._state = None + self._state_attrs = None + self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE + if Capability.switch_level in device.capabilities: + self._supported_features |= SUPPORT_SET_POSITION + + async def async_close_cover(self, **kwargs): + """Close cover.""" + # Same command for all 3 supported capabilities + await self._device.close(set_status=True) + # State is set optimistically in the commands above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state(True) + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + # Same for all capability types + await self._device.open(set_status=True) + # State is set optimistically in the commands above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state(True) + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + if not self._supported_features & SUPPORT_SET_POSITION: + return + # Do not set_status=True as device will report progress. + await self._device.set_level(kwargs[ATTR_POSITION], 0) + + async def async_update(self): + """Update the attrs of the cover.""" + from pysmartthings import Attribute, Capability + + value = None + if Capability.door_control in self._device.capabilities: + self._device_class = DEVICE_CLASS_DOOR + value = self._device.status.door + elif Capability.window_shade in self._device.capabilities: + self._device_class = DEVICE_CLASS_SHADE + value = self._device.status.window_shade + elif Capability.garage_door_control in self._device.capabilities: + self._device_class = DEVICE_CLASS_GARAGE + value = self._device.status.door + + self._state = VALUE_TO_STATE.get(value) + + self._state_attrs = {} + battery = self._device.status.attributes[Attribute.battery].value + if battery is not None: + self._state_attrs[ATTR_BATTERY_LEVEL] = battery + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._state == STATE_OPENING + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._state == STATE_CLOSING + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + if self._state == STATE_CLOSED: + return True + return None if self._state is None else False + + @property + def current_cover_position(self): + """Return current position of cover.""" + return self._device.status.level + + @property + def device_class(self): + """Define this cover as a garage door.""" + return self._device_class + + @property + def device_state_attributes(self): + """Get additional state attributes.""" + return self._state_attrs + + @property + def supported_features(self): + """Flag supported features.""" + return self._supported_features diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 32047c179b4..50beefdb5b2 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -43,8 +43,6 @@ CAPABILITY_TO_SENSORS = { Map('dishwasherJobState', "Dishwasher Job State", None, None), Map('completionTime', "Dishwasher Completion Time", None, DEVICE_CLASS_TIMESTAMP)], - 'doorControl': [ - Map('door', "Door", None, None)], 'dryerMode': [ Map('dryerMode', "Dryer Mode", None, None)], 'dryerOperatingState': [ @@ -62,8 +60,6 @@ CAPABILITY_TO_SENSORS = { 'Equivalent Carbon Dioxide Measurement', 'ppm', None)], 'formaldehydeMeasurement': [ Map('formaldehydeLevel', 'Formaldehyde Measurement', 'ppm', None)], - 'garageDoorControl': [ - Map('door', 'Garage Door', None, None)], 'illuminanceMeasurement': [ Map('illuminance', "Illuminance", 'lux', DEVICE_CLASS_ILLUMINANCE)], 'infraredLevel': [ @@ -143,9 +139,7 @@ CAPABILITY_TO_SENSORS = { Map('machineState', "Washer Machine State", None, None), Map('washerJobState', "Washer Job State", None, None), Map('completionTime', "Washer Completion Time", None, - DEVICE_CLASS_TIMESTAMP)], - 'windowShade': [ - Map('windowShade', 'Window Shade', None, None)] + DEVICE_CLASS_TIMESTAMP)] } UNITS = { diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py new file mode 100644 index 00000000000..6e7844e521c --- /dev/null +++ b/tests/components/smartthings/test_cover.py @@ -0,0 +1,196 @@ +""" +Test for the SmartThings cover platform. + +The only mocking required is of the underlying SmartThings API object so +real HTTP calls are not initiated during testing. +""" +from pysmartthings import Attribute, Capability + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, + STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING) +from homeassistant.components.smartthings import cover +from homeassistant.components.smartthings.const import ( + DOMAIN, SIGNAL_SMARTTHINGS_UPDATE) +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .conftest import setup_platform + + +async def test_async_setup_platform(): + """Test setup platform does nothing (it uses config entries).""" + await cover.async_setup_platform(None, None, None) + + +async def test_entity_and_device_attributes(hass, device_factory): + """Test the attributes of the entity are correct.""" + # Arrange + device = device_factory('Garage', [Capability.garage_door_control], + {Attribute.door: 'open'}) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = await hass.helpers.device_registry.async_get_registry() + # Act + await setup_platform(hass, COVER_DOMAIN, device) + # Assert + entry = entity_registry.async_get('cover.garage') + assert entry + assert entry.unique_id == device.device_id + + entry = device_registry.async_get_device( + {(DOMAIN, device.device_id)}, []) + assert entry + assert entry.name == device.label + assert entry.model == device.device_type_name + assert entry.manufacturer == 'Unavailable' + + +async def test_open(hass, device_factory): + """Test the cover opens doors, garages, and shades successfully.""" + # Arrange + devices = { + device_factory('Door', [Capability.door_control], + {Attribute.door: 'closed'}), + device_factory('Garage', [Capability.garage_door_control], + {Attribute.door: 'closed'}), + device_factory('Shade', [Capability.window_shade], + {Attribute.window_shade: 'closed'}) + } + await setup_platform(hass, COVER_DOMAIN, *devices) + entity_ids = [ + 'cover.door', + 'cover.garage', + 'cover.shade' + ] + # Act + await hass.services.async_call( + COVER_DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: entity_ids}, + blocking=True) + # Assert + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OPENING + + +async def test_close(hass, device_factory): + """Test the cover closes doors, garages, and shades successfully.""" + # Arrange + devices = { + device_factory('Door', [Capability.door_control], + {Attribute.door: 'open'}), + device_factory('Garage', [Capability.garage_door_control], + {Attribute.door: 'open'}), + device_factory('Shade', [Capability.window_shade], + {Attribute.window_shade: 'open'}) + } + await setup_platform(hass, COVER_DOMAIN, *devices) + entity_ids = [ + 'cover.door', + 'cover.garage', + 'cover.shade' + ] + # Act + await hass.services.async_call( + COVER_DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: entity_ids}, + blocking=True) + # Assert + for entity_id in entity_ids: + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_CLOSING + + +async def test_set_cover_position(hass, device_factory): + """Test the cover sets to the specific position.""" + # Arrange + device = device_factory( + 'Shade', + [Capability.window_shade, Capability.battery, + Capability.switch_level], + {Attribute.window_shade: 'opening', Attribute.battery: 95, + Attribute.level: 10}) + await setup_platform(hass, COVER_DOMAIN, device) + # Act + await hass.services.async_call( + COVER_DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_POSITION: 50}, blocking=True) + + state = hass.states.get('cover.shade') + # Result of call does not update state + assert state.state == STATE_OPENING + assert state.attributes[ATTR_BATTERY_LEVEL] == 95 + assert state.attributes[ATTR_CURRENT_POSITION] == 10 + # Ensure API called + # pylint: disable=protected-access + assert device._api.post_device_command.call_count == 1 # type: ignore + + +async def test_set_cover_position_unsupported(hass, device_factory): + """Test set position does nothing when not supported by device.""" + # Arrange + device = device_factory( + 'Shade', + [Capability.window_shade], + {Attribute.window_shade: 'opening'}) + await setup_platform(hass, COVER_DOMAIN, device) + # Act + await hass.services.async_call( + COVER_DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_POSITION: 50}, blocking=True) + + # Ensure API was notcalled + # pylint: disable=protected-access + assert device._api.post_device_command.call_count == 0 # type: ignore + + +async def test_update_to_open_from_signal(hass, device_factory): + """Test the cover updates to open when receiving a signal.""" + # Arrange + device = device_factory('Garage', [Capability.garage_door_control], + {Attribute.door: 'opening'}) + await setup_platform(hass, COVER_DOMAIN, device) + device.status.update_attribute_value(Attribute.door, 'open') + assert hass.states.get('cover.garage').state == STATE_OPENING + # Act + async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, + [device.device_id]) + # Assert + await hass.async_block_till_done() + state = hass.states.get('cover.garage') + assert state is not None + assert state.state == STATE_OPEN + + +async def test_update_to_closed_from_signal(hass, device_factory): + """Test the cover updates to closed when receiving a signal.""" + # Arrange + device = device_factory('Garage', [Capability.garage_door_control], + {Attribute.door: 'closing'}) + await setup_platform(hass, COVER_DOMAIN, device) + device.status.update_attribute_value(Attribute.door, 'closed') + assert hass.states.get('cover.garage').state == STATE_CLOSING + # Act + async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, + [device.device_id]) + # Assert + await hass.async_block_till_done() + state = hass.states.get('cover.garage') + assert state is not None + assert state.state == STATE_CLOSED + + +async def test_unload_config_entry(hass, device_factory): + """Test the lock is removed when the config entry is unloaded.""" + # Arrange + device = device_factory('Garage', [Capability.garage_door_control], + {Attribute.door: 'open'}) + config_entry = await setup_platform(hass, COVER_DOMAIN, device) + # Act + await hass.config_entries.async_forward_entry_unload( + config_entry, COVER_DOMAIN) + # Assert + assert not hass.states.get('cover.garage') From db4c06c8fe5e40cb7e9835eedac66c1bc825b2fc Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 25 Feb 2019 19:34:06 +0100 Subject: [PATCH 163/253] Add ESPHome User-Defined Services (#21409) * Add ESPHome User-Defined Services * Update requirements_all.txt --- homeassistant/components/esphome/__init__.py | 87 +++++++++++++++++--- requirements_all.txt | 2 +- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 004162341b1..51f565a0980 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -1,7 +1,7 @@ """Support for esphome devices.""" import asyncio import logging -from typing import Any, Dict, List, Optional, TYPE_CHECKING, Callable +from typing import Any, Dict, List, Optional, TYPE_CHECKING, Callable, Tuple import attr import voluptuous as vol @@ -13,6 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, \ from homeassistant.core import callback, Event, State import homeassistant.helpers.device_registry as dr from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers import template from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ async_dispatcher_send @@ -28,9 +29,10 @@ from .config_flow import EsphomeFlowHandler # noqa if TYPE_CHECKING: from aioesphomeapi import APIClient, EntityInfo, EntityState, DeviceInfo, \ - ServiceCall + ServiceCall, UserService -REQUIREMENTS = ['aioesphomeapi==1.5.0'] +DOMAIN = 'esphome' +REQUIREMENTS = ['aioesphomeapi==1.6.0'] _LOGGER = logging.getLogger(__name__) @@ -69,6 +71,7 @@ class RuntimeEntryData: reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None) state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + services = attr.ib(type=Dict[int, 'UserService'], factory=dict) available = attr.ib(type=bool, default=False) device_info = attr.ib(type='DeviceInfo', default=None) cleanup_callbacks = attr.ib(type=List[Callable[[], None]], factory=list) @@ -105,14 +108,16 @@ class RuntimeEntryData: signal = DISPATCHER_ON_DEVICE_UPDATE.format(entry_id=self.entry_id) async_dispatcher_send(hass, signal) - async def async_load_from_store(self) -> List['EntityInfo']: + async def async_load_from_store(self) -> Tuple[List['EntityInfo'], + List['UserService']]: """Load the retained data from store and return de-serialized data.""" # pylint: disable= redefined-outer-name - from aioesphomeapi import COMPONENT_TYPE_TO_INFO, DeviceInfo + from aioesphomeapi import COMPONENT_TYPE_TO_INFO, DeviceInfo, \ + UserService restored = await self.store.async_load() if restored is None: - return [] + return [], [] self.device_info = _attr_obj_from_dict(DeviceInfo, **restored.pop('device_info')) @@ -123,17 +128,23 @@ class RuntimeEntryData: for info in restored_infos: cls = COMPONENT_TYPE_TO_INFO[comp_type] infos.append(_attr_obj_from_dict(cls, **info)) - return infos + services = [] + for service in restored.get('services', []): + services.append(UserService.from_dict(service)) + return infos, services async def async_save_to_store(self) -> None: """Generate dynamic data to store and save it to the filesystem.""" store_data = { - 'device_info': attr.asdict(self.device_info) + 'device_info': attr.asdict(self.device_info), + 'services': [] } for comp_type, infos in self.info.items(): store_data[comp_type] = [attr.asdict(info) for info in infos.values()] + for service in self.services.values(): + store_data['services'].append(service.to_dict()) await self.store.async_save(store_data) @@ -233,8 +244,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry_data.device_info) entry_data.async_update_device_state(hass) - entity_infos = await cli.list_entities() + entity_infos, services = await cli.list_entities_services() entry_data.async_update_static_infos(hass, entity_infos) + await _setup_services(hass, entry_data, services) await cli.subscribe_states(async_on_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states( @@ -277,8 +289,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry, component)) await asyncio.wait(tasks) - infos = await entry_data.async_load_from_store() + infos, services = await entry_data.async_load_from_store() entry_data.async_update_static_infos(hass, infos) + await _setup_services(hass, entry_data, services) # If first connect fails, the next re-connect will be scheduled # outside of _pending_task, in order not to delay HA startup @@ -366,6 +379,60 @@ async def _async_setup_device_registry(hass: HomeAssistantType, ) +async def _register_service(hass: HomeAssistantType, + entry_data: RuntimeEntryData, + service: 'UserService'): + from aioesphomeapi import USER_SERVICE_ARG_BOOL, USER_SERVICE_ARG_INT, \ + USER_SERVICE_ARG_FLOAT, USER_SERVICE_ARG_STRING + service_name = '{}_{}'.format(entry_data.device_info.name, service.name) + schema = {} + for arg in service.args: + schema[vol.Required(arg.name)] = { + USER_SERVICE_ARG_BOOL: cv.boolean, + USER_SERVICE_ARG_INT: vol.Coerce(int), + USER_SERVICE_ARG_FLOAT: vol.Coerce(float), + USER_SERVICE_ARG_STRING: cv.string, + }[arg.type_] + + async def execute_service(call): + await entry_data.client.execute_service(service, call.data) + + hass.services.async_register(DOMAIN, service_name, execute_service, + vol.Schema(schema)) + + +async def _setup_services(hass: HomeAssistantType, + entry_data: RuntimeEntryData, + services: List['UserService']): + old_services = entry_data.services.copy() + to_unregister = [] + to_register = [] + for service in services: + if service.key in old_services: + # Already exists + matching = old_services.pop(service.key) + if matching != service: + # Need to re-register + to_unregister.append(matching) + to_register.append(service) + else: + # New service + to_register.append(service) + + for service in old_services.values(): + to_unregister.append(service) + + entry_data.services = {serv.key: serv for serv in services} + + for service in to_unregister: + service_name = '{}_{}'.format(entry_data.device_info.name, + service.name) + hass.services.async_remove(DOMAIN, service_name) + + for service in to_register: + await _register_service(hass, entry_data, service) + + async def _cleanup_instance(hass: HomeAssistantType, entry: ConfigEntry) -> None: """Cleanup the esphome client if it exists.""" diff --git a/requirements_all.txt b/requirements_all.txt index 1e9595418ca..48a2e6990c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -102,7 +102,7 @@ aioautomatic==0.6.5 aiodns==1.1.1 # homeassistant.components.esphome -aioesphomeapi==1.5.0 +aioesphomeapi==1.6.0 # homeassistant.components.freebox aiofreepybox==0.0.6 From 6626e5c4a4ac8684329298ef28a83a2175f9aafa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 25 Feb 2019 10:35:03 -0800 Subject: [PATCH 164/253] Handle GA Disconnect intent (#21387) * Handle GA Disconnect intent * Fixed lint error --- .../components/google_assistant/smart_home.py | 16 ++++++++++++++-- .../google_assistant/test_smart_home.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index bab63bdb7ae..8ea7a8aa7bc 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -187,7 +187,7 @@ async def async_handle_message(hass, config, message): """Handle incoming API messages.""" response = await _process(hass, config, message) - if 'errorCode' in response['payload']: + if response and 'errorCode' in response['payload']: _LOGGER.error('Error handling message %s: %s', message, response['payload']) @@ -215,7 +215,6 @@ async def _process(hass, config, message): try: result = await handler(hass, config, inputs[0].get('payload')) - return {'requestId': request_id, 'payload': result} except SmartHomeError as err: return { 'requestId': request_id, @@ -228,6 +227,10 @@ async def _process(hass, config, message): 'payload': {'errorCode': ERR_UNKNOWN_ERROR} } + if result is None: + return None + return {'requestId': request_id, 'payload': result} + @HANDLERS.register('action.devices.SYNC') async def async_devices_sync(hass, config, payload): @@ -337,6 +340,15 @@ async def handle_devices_execute(hass, config, payload): return {'commands': final_results} +@HANDLERS.register('action.devices.DISCONNECT') +async def async_devices_disconnect(hass, config, payload): + """Handle action.devices.DISCONNECT request. + + https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect + """ + return None + + def turned_off_response(message): """Return a device turned off response.""" return { diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index d7c6094bebb..05ae0809527 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -316,3 +316,15 @@ async def test_empty_name_doesnt_sync(hass): 'devices': [] } } + + +async def test_query_disconnect(hass): + """Test a disconnect message.""" + result = await sh.async_handle_message(hass, BASIC_CONFIG, { + 'inputs': [ + {'intent': 'action.devices.DISCONNECT'} + ], + 'requestId': REQ_ID + }) + + assert result is None From 4e9d0ebc63219a268b3819d492c8c6ad4c04fbbd Mon Sep 17 00:00:00 2001 From: koreth Date: Mon, 25 Feb 2019 12:09:58 -0800 Subject: [PATCH 165/253] Fix double events on Lutron Pico keypads (#21408) * Fix double events on Lutron Pico keypads (#21235) * Replace "else" with default value; remove explanatory comments --- homeassistant/components/lutron/__init__.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index e4ebec4cc5a..fae44d3584d 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -134,19 +134,18 @@ class LutronButton: """Fire an event about a button being pressed or released.""" from pylutron import Button + # Events per button type: + # RaiseLower -> pressed/released + # SingleAction -> single + action = None if self._has_release_event: - # A raise/lower button; we will get callbacks when the button is - # pressed and when it's released, so fire events for each. if event == Button.Event.PRESSED: action = 'pressed' else: action = 'released' - else: - # A single-action button; the Lutron controller won't tell us - # when the button is released, so use a different action name - # than for buttons where we expect a release event. + elif event == Button.Event.PRESSED: action = 'single' - data = {ATTR_ID: self._id, ATTR_ACTION: action} - - self._hass.bus.fire(self._event, data) + if action: + data = {ATTR_ID: self._id, ATTR_ACTION: action} + self._hass.bus.fire(self._event, data) From 095a0d19d1b58b2f119d17851696803d0afbbfb2 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 25 Feb 2019 22:03:15 +0100 Subject: [PATCH 166/253] Fix Sonos snapshot/restore (#21411) --- .../components/sonos/media_player.py | 184 +++++++++--------- tests/components/sonos/test_media_player.py | 41 ++-- 2 files changed, 116 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index ee2f9e6b4dc..4a02cf2676f 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -195,35 +195,30 @@ def _setup_platform(hass, config, add_entities, discovery_info): if entity_ids: entities = [e for e in entities if e.entity_id in entity_ids] - if service.service == SERVICE_JOIN: - master = [e for e in hass.data[DATA_SONOS].entities - if e.entity_id == service.data[ATTR_MASTER]] - if master: - with hass.data[DATA_SONOS].topology_lock: - master[0].join(entities) - return - - if service.service == SERVICE_UNJOIN: - with hass.data[DATA_SONOS].topology_lock: - for entity in entities: - entity.unjoin() - return - - for entity in entities: + with hass.data[DATA_SONOS].topology_lock: if service.service == SERVICE_SNAPSHOT: - entity.snapshot(service.data[ATTR_WITH_GROUP]) + snapshot(entities, service.data[ATTR_WITH_GROUP]) elif service.service == SERVICE_RESTORE: - entity.restore(service.data[ATTR_WITH_GROUP]) - elif service.service == SERVICE_SET_TIMER: - entity.set_sleep_timer(service.data[ATTR_SLEEP_TIME]) - elif service.service == SERVICE_CLEAR_TIMER: - entity.clear_sleep_timer() - elif service.service == SERVICE_UPDATE_ALARM: - entity.set_alarm(**service.data) - elif service.service == SERVICE_SET_OPTION: - entity.set_option(**service.data) + restore(entities, service.data[ATTR_WITH_GROUP]) + elif service.service == SERVICE_JOIN: + master = [e for e in hass.data[DATA_SONOS].entities + if e.entity_id == service.data[ATTR_MASTER]] + if master: + master[0].join(entities) + else: + for entity in entities: + if service.service == SERVICE_UNJOIN: + entity.unjoin() + elif service.service == SERVICE_SET_TIMER: + entity.set_sleep_timer(service.data[ATTR_SLEEP_TIME]) + elif service.service == SERVICE_CLEAR_TIMER: + entity.clear_sleep_timer() + elif service.service == SERVICE_UPDATE_ALARM: + entity.set_alarm(**service.data) + elif service.service == SERVICE_SET_OPTION: + entity.set_option(**service.data) - entity.schedule_update_ha_state(True) + entity.schedule_update_ha_state(True) hass.services.register( DOMAIN, SERVICE_JOIN, service_handle, @@ -346,7 +341,7 @@ class SonosEntity(MediaPlayerDevice): self._shuffle = None self._name = None self._coordinator = None - self._sonos_group = None + self._sonos_group = [self] self._status = None self._media_duration = None self._media_position = None @@ -375,6 +370,10 @@ class SonosEntity(MediaPlayerDevice): """Return a unique ID.""" return self._unique_id + def __hash__(self): + """Return a hash of self.""" + return hash(self.unique_id) + @property def name(self): """Return the name of the entity.""" @@ -729,7 +728,7 @@ class SonosEntity(MediaPlayerDevice): for uid in (coordinator_uid, *slave_uids): entity = _get_entity_from_soco_uid(self.hass, uid) if entity: - sonos_group.append(entity.entity_id) + sonos_group.append(entity) self._coordinator = None self._sonos_group = sonos_group @@ -975,72 +974,6 @@ class SonosEntity(MediaPlayerDevice): self.soco.unjoin() self._coordinator = None - @soco_error() - def snapshot(self, with_group=True): - """Snapshot the player.""" - from pysonos.snapshot import Snapshot - - self._soco_snapshot = Snapshot(self.soco) - self._soco_snapshot.snapshot() - - if with_group: - self._snapshot_group = self.soco.group - if self._coordinator: - self._coordinator.snapshot(False) - else: - self._snapshot_group = None - - @soco_error() - def restore(self, with_group=True): - """Restore snapshot for the player.""" - from pysonos.exceptions import SoCoException - try: - # need catch exception if a coordinator is going to slave. - # this state will recover with group part. - self._soco_snapshot.restore(False) - except (TypeError, AttributeError, SoCoException): - _LOGGER.debug("Error on restore %s", self.entity_id) - - # restore groups - if with_group and self._snapshot_group: - old = self._snapshot_group - actual = self.soco.group - - ## - # Master have not change, update group - if old.coordinator == actual.coordinator: - if self.soco is not old.coordinator: - # restore state of the groups - self._coordinator.restore(False) - remove = actual.members - old.members - add = old.members - actual.members - - # remove new members - for soco_dev in list(remove): - soco_dev.unjoin() - - # add old members - for soco_dev in list(add): - soco_dev.join(old.coordinator) - return - - ## - # old is already master, rejoin - if old.coordinator.group.coordinator == old.coordinator: - self.soco.join(old.coordinator) - return - - ## - # restore old master, update group - old.coordinator.unjoin() - coordinator = _get_entity_from_soco_uid( - self.hass, old.coordinator.uid) - coordinator.restore(False) - - for s_dev in list(old.members): - if s_dev != old.coordinator: - s_dev.join(old.coordinator) - @soco_error() @soco_coordinator def set_sleep_timer(self, sleep_time): @@ -1089,7 +1022,9 @@ class SonosEntity(MediaPlayerDevice): @property def device_state_attributes(self): """Return entity specific state attributes.""" - attributes = {ATTR_SONOS_GROUP: self._sonos_group} + attributes = { + ATTR_SONOS_GROUP: [e.entity_id for e in self._sonos_group], + } if self._night_sound is not None: attributes[ATTR_NIGHT_SOUND] = self._night_sound @@ -1098,3 +1033,62 @@ class SonosEntity(MediaPlayerDevice): attributes[ATTR_SPEECH_ENHANCE] = self._speech_enhance return attributes + + +@soco_error() +def snapshot(entities, with_group): + """Snapshot all the entities and optionally their groups.""" + # pylint: disable=protected-access + from pysonos.snapshot import Snapshot + + # Find all affected players + entities = set(entities) + if with_group: + for entity in list(entities): + entities.update(entity._sonos_group) + + for entity in entities: + entity._soco_snapshot = Snapshot(entity.soco) + entity._soco_snapshot.snapshot() + if with_group: + entity._snapshot_group = entity._sonos_group.copy() + else: + entity._snapshot_group = None + + +@soco_error() +def restore(entities, with_group): + """Restore snapshots for all the entities.""" + # pylint: disable=protected-access + from pysonos.exceptions import SoCoException + + # Find all affected players + entities = set(e for e in entities if e._soco_snapshot) + if with_group: + for entity in [e for e in entities if e._snapshot_group]: + entities.update(entity._snapshot_group) + + # Pause all current coordinators + for entity in (e for e in entities if e.is_coordinator): + if entity.state == STATE_PLAYING: + entity.media_pause() + + # Bring back the original group topology and clear pysonos cache + if with_group: + for entity in (e for e in entities if e._snapshot_group): + if entity._snapshot_group[0] == entity: + entity.join(entity._snapshot_group) + entity.soco._zgs_cache.clear() + + # Restore slaves, then coordinators + slaves = [e for e in entities if not e.is_coordinator] + coordinators = [e for e in entities if e.is_coordinator] + for entity in slaves + coordinators: + try: + entity._soco_snapshot.restore() + except (TypeError, AttributeError, SoCoException) as ex: + # Can happen if restoring a coordinator onto a current slave + _LOGGER.warning("Error on restore %s: %s", entity.entity_id, ex) + + entity._soco_snapshot = None + entity._snapshot_group = None diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index d57c730a9f8..55743c4f843 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -49,6 +49,14 @@ class MusicLibraryMock(): return [] +class CacheMock(): + """Mock class for the _zgs_cache property on pysonos.SoCo object.""" + + def clear(self): + """Clear cache.""" + pass + + class SoCoMock(): """Mock class for the pysonos.SoCo object.""" @@ -63,6 +71,7 @@ class SoCoMock(): self.dialog_mode = False self.music_library = MusicLibraryMock() self.avTransport = AvTransportMock() + self._zgs_cache = CacheMock() def get_sonos_favorites(self): """Get favorites list from sonos.""" @@ -126,7 +135,7 @@ def add_entities_factory(hass): """Add entities factory.""" def add_entities(entities, update_befor_add=False): """Fake add entity.""" - hass.data[sonos.DATA_SONOS].entities = entities + hass.data[sonos.DATA_SONOS].entities = list(entities) return add_entities @@ -162,7 +171,7 @@ class TestSonosMediaPlayer(unittest.TestCase): 'host': '192.0.2.1' }) - entities = list(self.hass.data[sonos.DATA_SONOS].entities) + entities = self.hass.data[sonos.DATA_SONOS].entities assert len(entities) == 1 assert entities[0].name == 'Kitchen' @@ -242,7 +251,7 @@ class TestSonosMediaPlayer(unittest.TestCase): def test_ensure_setup_sonos_discovery(self, *args): """Test a single device using the autodiscovery provided by Sonos.""" sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass)) - entities = list(self.hass.data[sonos.DATA_SONOS].entities) + entities = self.hass.data[sonos.DATA_SONOS].entities assert len(entities) == 1 assert entities[0].name == 'Kitchen' @@ -254,7 +263,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1] + entity = self.hass.data[sonos.DATA_SONOS].entities[-1] entity.hass = self.hass entity.set_sleep_timer(30) @@ -268,7 +277,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1] + entity = self.hass.data[sonos.DATA_SONOS].entities[-1] entity.hass = self.hass entity.set_sleep_timer(None) @@ -282,7 +291,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1] + entity = self.hass.data[sonos.DATA_SONOS].entities[-1] entity.hass = self.hass alarm1 = alarms.Alarm(pysonos_mock) alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False, @@ -312,11 +321,14 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1] + entities = self.hass.data[sonos.DATA_SONOS].entities + entity = entities[-1] entity.hass = self.hass snapshotMock.return_value = True - entity.snapshot() + entity.soco.group = mock.MagicMock() + entity.soco.group.members = [e.soco for e in entities] + sonos.snapshot(entities, True) assert snapshotMock.call_count == 1 assert snapshotMock.call_args == mock.call() @@ -330,13 +342,14 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { 'host': '192.0.2.1' }) - entity = list(self.hass.data[sonos.DATA_SONOS].entities)[-1] + entities = self.hass.data[sonos.DATA_SONOS].entities + entity = entities[-1] entity.hass = self.hass restoreMock.return_value = True - entity._snapshot_coordinator = mock.MagicMock() - entity._snapshot_coordinator.soco_entity = SoCoMock('192.0.2.17') - entity._soco_snapshot = Snapshot(entity._player) - entity.restore() + entity._snapshot_group = mock.MagicMock() + entity._snapshot_group.members = [e.soco for e in entities] + entity._soco_snapshot = Snapshot(entity.soco) + sonos.restore(entities, True) assert restoreMock.call_count == 1 - assert restoreMock.call_args == mock.call(False) + assert restoreMock.call_args == mock.call() From f0268688be9c66228ebcc83053e9f7864c81656b Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Mon, 25 Feb 2019 20:59:02 -0600 Subject: [PATCH 167/253] Increase travis timeout (#21447) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 920e8b57047..be00f989290 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,7 @@ cache: - $HOME/.cache/pip install: pip install -U tox coveralls language: python -script: travis_wait 30 tox --develop +script: travis_wait 40 tox --develop services: - docker before_deploy: From f3c9327ccf6e03f359d78c35b220cdd54f1eb40e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Feb 2019 19:18:09 +0100 Subject: [PATCH 168/253] Rewrite of Toon component (#21186) * :tractor: Rewrite of Toon component * :fire: Removed manual state from list * :shirt: Addresses code review comments * :fire: Removes a log line that should not have been left behind * :shirt: Addresses linting warnings * :shirt: Addresses Hound CI warning * :shirt: Fixes small code styling issues * :sparkles: Sets an appropriate SCAN_INTERVAL * :sparkles: Sets min/max temperature for climate platform * :shirt: Makes imports more consistent with codebase * :ambulance: Fixes incorrect SCAN_INTERVAL value in climate platform * :ambulance: Uses OrderedDict for config_flow schema * :shirt: Adds return types for min/max temp * :tractor: Refactors entities into their actual devices * :arrow_up: Updates toonapilib to 3.0.7 * :tractor: Refactors binary sensor state inversion * :ambulance: Fixes states of OpenTherm connection and Hot Tap Water * :sparkles: Adds Boiler Preheat binary sensor * :sparkles: Adds Toon Thermostat Program binary sensor * :sparkles: Adds Boiler Modulation Level sensor * :sparkles: Adds Daily Power Cost sensor * :fire: Cleanup of Toon Thermostat climate attributes * :tractor: Adjusts config_flow with Tenant selection * :raising_hand: Adds myself to codeowners file as maintainer * :arrow_up: Gen requirements * :arrow_up: Updates toonapilib to 3.0.9 * :umbrella: Adds config_flow tests --- CODEOWNERS | 1 + .../components/toon/.translations/en.json | 34 ++ homeassistant/components/toon/__init__.py | 261 +++++++++------ .../components/toon/binary_sensor.py | 127 +++++++ homeassistant/components/toon/climate.py | 127 ++++--- homeassistant/components/toon/config_flow.py | 158 +++++++++ homeassistant/components/toon/const.py | 21 ++ homeassistant/components/toon/sensor.py | 316 ++++++++---------- homeassistant/components/toon/strings.json | 34 ++ homeassistant/components/toon/switch.py | 68 ---- homeassistant/config_entries.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/toon/__init__.py | 1 + tests/components/toon/test_config_flow.py | 177 ++++++++++ 16 files changed, 943 insertions(+), 389 deletions(-) create mode 100644 homeassistant/components/toon/.translations/en.json create mode 100644 homeassistant/components/toon/binary_sensor.py create mode 100644 homeassistant/components/toon/config_flow.py create mode 100644 homeassistant/components/toon/const.py create mode 100644 homeassistant/components/toon/strings.json delete mode 100644 homeassistant/components/toon/switch.py create mode 100644 tests/components/toon/__init__.py create mode 100644 tests/components/toon/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 6fb00f382ea..9d0476e7a37 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -265,6 +265,7 @@ homeassistant/components/tibber/* @danielhiversen homeassistant/components/*/tibber.py @danielhiversen homeassistant/components/tradfri/* @ggravlingen homeassistant/components/*/tradfri.py @ggravlingen +homeassistant/components/toon/* @frenck # U homeassistant/components/unifi/* @kane610 diff --git a/homeassistant/components/toon/.translations/en.json b/homeassistant/components/toon/.translations/en.json new file mode 100644 index 00000000000..80d71d4e421 --- /dev/null +++ b/homeassistant/components/toon/.translations/en.json @@ -0,0 +1,34 @@ +{ + "config": { + "title": "Toon", + "step": { + "authenticate": { + "title": "Link your Toon account", + "description": "Authenticate with your Eneco Toon account (not the developer account).", + "data": { + "username": "Username", + "password": "Password", + "tenant": "Tenant" + } + }, + "display": { + "title": "Select display", + "description": "Select the Toon display to connect with.", + "data": { + "display": "Choose display" + } + } + }, + "error": { + "credentials": "The provided credentials are invalid.", + "display_exists": "The selected display is already configured." + }, + "abort": { + "client_id": "The client ID from the configuration is invalid.", + "client_secret": "The client secret from the configuration is invalid.", + "unknown_auth_fail": "Unexpected error occured, while authenticating.", + "no_agreements": "This account has no Toon displays.", + "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/)." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 96d8b4e6d15..fce0bc4ed2a 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -1,142 +1,195 @@ """Support for Toon van Eneco devices.""" -from datetime import datetime, timedelta import logging +from typing import Any, Dict import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.util import Throttle +from homeassistant.helpers import (config_validation as cv, + device_registry as dr) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, HomeAssistantType -REQUIREMENTS = ['toonlib==1.1.3'] +from . import config_flow # noqa pylint_disable=unused-import +from .const import ( + CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, + DATA_TOON_CLIENT, DATA_TOON_CONFIG, DOMAIN) + +REQUIREMENTS = ['toonapilib==3.0.9'] _LOGGER = logging.getLogger(__name__) -CONF_GAS = 'gas' -CONF_SOLAR = 'solar' - -DEFAULT_GAS = True -DEFAULT_SOLAR = False -DOMAIN = 'toon' - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) - -TOON_HANDLE = 'toon_handle' - # Validation of the user's configuration CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_GAS, default=DEFAULT_GAS): cv.boolean, - vol.Optional(CONF_SOLAR, default=DEFAULT_SOLAR): cv.boolean, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, }), }, extra=vol.ALLOW_EXTRA) -def setup(hass, config): - """Set up the Toon component.""" - from toonlib import InvalidCredentials - gas = config[DOMAIN][CONF_GAS] - solar = config[DOMAIN][CONF_SOLAR] - username = config[DOMAIN][CONF_USERNAME] - password = config[DOMAIN][CONF_PASSWORD] +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up the Toon components.""" + if DOMAIN not in config: + return True - try: - hass.data[TOON_HANDLE] = ToonDataStore(username, password, gas, solar) - except InvalidCredentials: - return False + conf = config[DOMAIN] - for platform in ('climate', 'sensor', 'switch'): - load_platform(hass, platform, DOMAIN, {}, config) + # Store config to be used during entry setup + hass.data[DATA_TOON_CONFIG] = conf return True -class ToonDataStore: - """An object to store the Toon data.""" +async def async_setup_entry(hass: HomeAssistantType, + entry: ConfigType) -> bool: + """Set up Toon from a config entry.""" + from toonapilib import Toon - def __init__( - self, username, password, gas=DEFAULT_GAS, solar=DEFAULT_SOLAR): - """Initialize Toon.""" - from toonlib import Toon + conf = hass.data.get(DATA_TOON_CONFIG) - toon = Toon(username, password) + toon = Toon(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], + conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET], + tenant_id=entry.data[CONF_TENANT], + display_common_name=entry.data[CONF_DISPLAY]) + hass.data.setdefault(DATA_TOON_CLIENT, {})[entry.entry_id] = toon + + # Register device for the Meter Adapter, since it will have no entities. + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={ + (DOMAIN, toon.agreement.id, 'meter_adapter'), + }, + manufacturer='Eneco', + name="Meter Adapter", + via_hub=(DOMAIN, toon.agreement.id) + ) + + for component in 'binary_sensor', 'climate', 'sensor': + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component)) + + return True + + +class ToonEntity(Entity): + """Defines a base Toon entity.""" + + def __init__(self, toon, name: str, icon: str) -> None: + """Initialize the Toon entity.""" + self._name = name + self._state = None + self._icon = icon self.toon = toon - self.gas = gas - self.solar = solar - self.data = {} - self.last_update = datetime.min - self.update() + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update Toon data.""" - self.last_update = datetime.now() + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + return self._icon - self.data['power_current'] = self.toon.power.value - self.data['power_today'] = round( - (float(self.toon.power.daily_usage) + - float(self.toon.power.daily_usage_low)) / 1000, 2) - self.data['temp'] = self.toon.temperature - if self.toon.thermostat_state: - self.data['state'] = self.toon.thermostat_state.name - else: - self.data['state'] = 'Manual' +class ToonDisplayDeviceEntity(ToonEntity): + """Defines a Toon display device entity.""" - self.data['setpoint'] = float( - self.toon.thermostat_info.current_set_point) / 100 - self.data['gas_current'] = self.toon.gas.value - self.data['gas_today'] = round(float(self.toon.gas.daily_usage) / - 1000, 2) + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this thermostat.""" + agreement = self.toon.agreement + model = agreement.display_hardware_version.rpartition('/')[0] + sw_version = agreement.display_software_version.rpartition('/')[-1] + return { + 'identifiers': { + (DOMAIN, agreement.id), + }, + 'name': 'Toon Display', + 'manufacturer': 'Eneco', + 'model': model, + 'sw_version': sw_version, + } - for plug in self.toon.smartplugs: - self.data[plug.name] = { - 'current_power': plug.current_usage, - 'today_energy': round(float(plug.daily_usage) / 1000, 2), - 'current_state': plug.current_state, - 'is_connected': plug.is_connected, - } - self.data['solar_maximum'] = self.toon.solar.maximum - self.data['solar_produced'] = self.toon.solar.produced - self.data['solar_value'] = self.toon.solar.value - self.data['solar_average_produced'] = self.toon.solar.average_produced - self.data['solar_meter_reading_low_produced'] = \ - self.toon.solar.meter_reading_low_produced - self.data['solar_meter_reading_produced'] = \ - self.toon.solar.meter_reading_produced - self.data['solar_daily_cost_produced'] = \ - self.toon.solar.daily_cost_produced +class ToonElectricityMeterDeviceEntity(ToonEntity): + """Defines a Electricity Meter device entity.""" - for detector in self.toon.smokedetectors: - value = '{}_smoke_detector'.format(detector.name) - self.data[value] = { - 'smoke_detector': detector.battery_level, - 'device_type': detector.device_type, - 'is_connected': detector.is_connected, - 'last_connected_change': detector.last_connected_change, - } + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this entity.""" + return { + 'name': 'Electricity Meter', + 'identifiers': { + (DOMAIN, self.toon.agreement.id, 'electricity'), + }, + 'via_hub': (DOMAIN, self.toon.agreement.id, 'meter_adapter'), + } - def set_state(self, state): - """Push a new state to the Toon unit.""" - self.toon.thermostat_state = state - def set_temp(self, temp): - """Push a new temperature to the Toon unit.""" - self.toon.thermostat = temp +class ToonGasMeterDeviceEntity(ToonEntity): + """Defines a Gas Meter device entity.""" - def get_data(self, data_id, plug_name=None): - """Get the cached data.""" - data = {'error': 'no data'} - if plug_name: - if data_id in self.data[plug_name]: - data = self.data[plug_name][data_id] - else: - if data_id in self.data: - data = self.data[data_id] - return data + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this entity.""" + via_hub = 'meter_adapter' + if self.toon.gas.is_smart: + via_hub = 'electricity' + + return { + 'name': 'Gas Meter', + 'identifiers': { + (DOMAIN, self.toon.agreement.id, 'gas'), + }, + 'via_hub': (DOMAIN, self.toon.agreement.id, via_hub), + } + + +class ToonSolarDeviceEntity(ToonEntity): + """Defines a Solar Device device entity.""" + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this entity.""" + return { + 'name': 'Solar Panels', + 'identifiers': { + (DOMAIN, self.toon.agreement.id, 'solar'), + }, + 'via_hub': (DOMAIN, self.toon.agreement.id, 'meter_adapter'), + } + + +class ToonBoilerModuleDeviceEntity(ToonEntity): + """Defines a Boiler Module device entity.""" + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this entity.""" + return { + 'name': 'Boiler Module', + 'manufacturer': 'Eneco', + 'identifiers': { + (DOMAIN, self.toon.agreement.id, 'boiler_module'), + }, + 'via_hub': (DOMAIN, self.toon.agreement.id), + } + + +class ToonBoilerDeviceEntity(ToonEntity): + """Defines a Boiler device entity.""" + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this entity.""" + return { + 'name': 'Boiler', + 'identifiers': { + (DOMAIN, self.toon.agreement.id, 'boiler'), + }, + 'via_hub': (DOMAIN, self.toon.agreement.id, 'boiler_module'), + } diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py new file mode 100644 index 00000000000..891a72daeed --- /dev/null +++ b/homeassistant/components/toon/binary_sensor.py @@ -0,0 +1,127 @@ +"""Support for Toon binary sensors.""" + +from datetime import timedelta +import logging +from typing import Any + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import (ToonEntity, ToonDisplayDeviceEntity, ToonBoilerDeviceEntity, + ToonBoilerModuleDeviceEntity) +from .const import DATA_TOON_CLIENT, DOMAIN + +DEPENDENCIES = ['toon'] + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=30) + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, + async_add_entities) -> None: + """Set up a Toon binary sensor based on a config entry.""" + toon = hass.data[DATA_TOON_CLIENT][entry.entry_id] + + sensors = [ + ToonBoilerModuleBinarySensor(toon, 'thermostat_info', + 'boiler_connected', None, + 'Boiler Module Connection', + 'mdi:check-network-outline', + 'connectivity'), + + ToonDisplayBinarySensor(toon, 'thermostat_info', 'active_state', 4, + "Toon Holiday Mode", 'mdi:airport', None), + + ToonDisplayBinarySensor(toon, 'thermostat_info', 'next_program', None, + "Toon Program", 'mdi:calendar-clock', None), + ] + + if toon.thermostat_info.have_ot_boiler: + sensors.extend([ + ToonBoilerBinarySensor(toon, 'thermostat_info', + 'ot_communication_error', '0', + "OpenTherm Connection", + 'mdi:check-network-outline', + 'connectivity'), + ToonBoilerBinarySensor(toon, 'thermostat_info', 'error_found', 255, + "Boiler Status", 'mdi:alert', 'problem', + inverted=True), + ToonBoilerBinarySensor(toon, 'thermostat_info', 'burner_info', + None, "Boiler Burner", 'mdi:fire', None), + ToonBoilerBinarySensor(toon, 'thermostat_info', 'burner_info', '2', + "Hot Tap Water", 'mdi:water-pump', None), + ToonBoilerBinarySensor(toon, 'thermostat_info', 'burner_info', '3', + "Boiler Preheating", 'mdi:fire', None), + ]) + + async_add_entities(sensors) + + +class ToonBinarySensor(ToonEntity, BinarySensorDevice): + """Defines an Toon binary sensor.""" + + def __init__(self, toon, section: str, measurement: str, on_value: Any, + name: str, icon: str, device_class: str, + inverted: bool = False) -> None: + """Initialize the Toon sensor.""" + self._state = inverted + self._device_class = device_class + self.section = section + self.measurement = measurement + self.on_value = on_value + self.inverted = inverted + + super().__init__(toon, name, icon) + + @property + def unique_id(self) -> str: + """Return the unique ID for this binary sensor.""" + return '_'.join([DOMAIN, self.toon.agreement.id, 'binary_sensor', + self.section, self.measurement, str(self.on_value)]) + + @property + def device_class(self) -> str: + """Return the device class.""" + return self._device_class + + @property + def is_on(self) -> bool: + """Return the status of the binary sensor.""" + if self.on_value is not None: + value = self._state == self.on_value + elif self._state is None: + value = False + else: + value = bool(max(0, int(self._state))) + + if self.inverted: + return not value + + return value + + async def async_update(self) -> None: + """Get the latest data from the binary sensor.""" + section = getattr(self.toon, self.section) + self._state = getattr(section, self.measurement) + + +class ToonBoilerBinarySensor(ToonBinarySensor, ToonBoilerDeviceEntity): + """Defines a Boiler binary sensor.""" + + pass + + +class ToonDisplayBinarySensor(ToonBinarySensor, ToonDisplayDeviceEntity): + """Defines a Toon Display binary sensor.""" + + pass + + +class ToonBoilerModuleBinarySensor(ToonBinarySensor, + ToonBoilerModuleDeviceEntity): + """Defines a Boiler module binary sensor.""" + + pass diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index c07ccf79d26..2e564b8457a 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -1,90 +1,129 @@ -"""Support for Toon van Eneco Thermostats.""" +"""Support for Toon thermostat.""" + +from datetime import timedelta +import logging +from typing import Any, Dict, List + from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - STATE_COOL, STATE_ECO, STATE_HEAT, STATE_AUTO, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) -import homeassistant.components.toon as toon_main + STATE_AUTO, STATE_COOL, STATE_ECO, STATE_HEAT, SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.helpers.typing import HomeAssistantType + +from . import ToonDisplayDeviceEntity +from .const import DATA_TOON_CLIENT, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN + +DEPENDENCIES = ['toon'] + +_LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=30) + HA_TOON = { STATE_AUTO: 'Comfort', STATE_HEAT: 'Home', STATE_ECO: 'Away', STATE_COOL: 'Sleep', } + TOON_HA = {value: key for key, value in HA_TOON.items()} -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Toon climate device.""" - add_entities([ThermostatDevice(hass)], True) +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, + async_add_entities) -> None: + """Set up a Toon binary sensors based on a config entry.""" + toon = hass.data[DATA_TOON_CLIENT][entry.entry_id] + async_add_entities([ToonThermostatDevice(toon)], True) -class ThermostatDevice(ClimateDevice): +class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateDevice): """Representation of a Toon climate device.""" - def __init__(self, hass): + def __init__(self, toon) -> None: """Initialize the Toon climate device.""" - self._name = 'Toon van Eneco' - self.hass = hass - self.thermos = hass.data[toon_main.TOON_HANDLE] - self._state = None - self._temperature = None - self._setpoint = None - self._operation_list = [ - STATE_AUTO, - STATE_HEAT, - STATE_ECO, - STATE_COOL, - ] + + self._current_temperature = None + self._target_temperature = None + self._next_target_temperature = None + + self._heating_type = None + + super().__init__(toon, "Toon Thermostat", 'mdi:thermostat') @property - def supported_features(self): + def unique_id(self) -> str: + """Return the unique ID for this thermostat.""" + return '_'.join([DOMAIN, self.toon.agreement.id, 'climate']) + + @property + def supported_features(self) -> int: """Return the list of supported features.""" return SUPPORT_FLAGS @property - def name(self): - """Return the name of this thermostat.""" - return self._name - - @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" + def temperature_unit(self) -> str: + """Return the unit of measurement.""" return TEMP_CELSIUS @property - def current_operation(self): + def current_operation(self) -> str: """Return current operation i.e. comfort, home, away.""" - return TOON_HA.get(self.thermos.get_data('state')) + return TOON_HA.get(self._state) @property - def operation_list(self): + def operation_list(self) -> List[str]: """Return a list of available operation modes.""" - return self._operation_list + return list(HA_TOON.keys()) @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" - return self.thermos.get_data('temp') + return self._current_temperature @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the temperature we try to reach.""" - return self.thermos.get_data('setpoint') + return self._target_temperature - def set_temperature(self, **kwargs): + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return DEFAULT_MIN_TEMP + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return DEFAULT_MAX_TEMP + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return the current state of the burner.""" + return { + 'heating_type': self._heating_type, + } + + def set_temperature(self, **kwargs) -> None: """Change the setpoint of the thermostat.""" - temp = kwargs.get(ATTR_TEMPERATURE) - self.thermos.set_temp(temp) + temperature = kwargs.get(ATTR_TEMPERATURE) + self.toon.thermostat = temperature - def set_operation_mode(self, operation_mode): + def set_operation_mode(self, operation_mode: str) -> None: """Set new operation mode.""" - self.thermos.set_state(HA_TOON[operation_mode]) + self.toon.thermostat_state = HA_TOON[operation_mode] - def update(self): + async def async_update(self) -> None: """Update local state.""" - self.thermos.update() + if self.toon.thermostat_state is None: + self._state = None + else: + self._state = self.toon.thermostat_state.name + + self._current_temperature = self.toon.temperature + self._target_temperature = self.toon.thermostat + self._heating_type = self.toon.agreement.heating_type diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py new file mode 100644 index 00000000000..cdb8a0f2257 --- /dev/null +++ b/homeassistant/components/toon/config_flow.py @@ -0,0 +1,158 @@ +"""Config flow to configure the Toon component.""" +from collections import OrderedDict +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback + +from .const import ( + CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, + DATA_TOON_CONFIG, DOMAIN) + +_LOGGER = logging.getLogger(__name__) + + +@callback +def configured_displays(hass): + """Return a set of configured Toon displays.""" + return set( + entry.data[CONF_DISPLAY] + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +@config_entries.HANDLERS.register(DOMAIN) +class ToonFlowHandler(config_entries.ConfigFlow): + """Handle a Toon config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize the Toon flow.""" + self.displays = None + self.username = None + self.password = None + self.tenant = None + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + app = self.hass.data.get(DATA_TOON_CONFIG, {}) + + if not app: + return self.async_abort(reason='no_app') + + return await self.async_step_authenticate(user_input) + + async def _show_authenticaticate_form(self, errors=None): + """Show the authentication form to the user.""" + fields = OrderedDict() + fields[vol.Required(CONF_USERNAME)] = str + fields[vol.Required(CONF_PASSWORD)] = str + fields[vol.Optional(CONF_TENANT)] = vol.In([ + 'eneco', 'electrabel', 'viesgo' + ]) + + return self.async_show_form( + step_id='authenticate', + data_schema=vol.Schema(fields), + errors=errors if errors else {}, + ) + + async def async_step_authenticate(self, user_input=None): + """Attempt to authenticate with the Toon account.""" + from toonapilib import Toon + from toonapilib.toonapilibexceptions import (InvalidConsumerSecret, + InvalidConsumerKey, + InvalidCredentials, + AgreementsRetrievalError) + + if user_input is None: + return await self._show_authenticaticate_form() + + app = self.hass.data.get(DATA_TOON_CONFIG, {}) + try: + toon = Toon(user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + app[CONF_CLIENT_ID], + app[CONF_CLIENT_SECRET], + tenant_id=user_input[CONF_TENANT]) + + displays = toon.display_names + + except InvalidConsumerKey: + return self.async_abort(reason='client_id') + + except InvalidConsumerSecret: + return self.async_abort(reason='client_secret') + + except InvalidCredentials: + return await self._show_authenticaticate_form({ + 'base': 'credentials' + }) + + except AgreementsRetrievalError: + return self.async_abort(reason='no_agreements') + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error while authenticating") + return self.async_abort(reason='unknown_auth_fail') + + self.displays = displays + self.username = user_input[CONF_USERNAME] + self.password = user_input[CONF_PASSWORD] + self.tenant = user_input[CONF_TENANT] + + return await self.async_step_display() + + async def _show_display_form(self, errors=None): + """Show the select display form to the user.""" + fields = OrderedDict() + fields[vol.Required(CONF_DISPLAY)] = vol.In(self.displays) + + return self.async_show_form( + step_id='display', + data_schema=vol.Schema(fields), + errors=errors if errors else {}, + ) + + async def async_step_display(self, user_input=None): + """Select Toon display to add.""" + from toonapilib import Toon + + if not self.displays: + return self.async_abort(reason='no_displays') + + if user_input is None: + return await self._show_display_form() + + if user_input[CONF_DISPLAY] in configured_displays(self.hass): + return await self._show_display_form({ + 'base': 'display_exists' + }) + + app = self.hass.data.get(DATA_TOON_CONFIG, {}) + try: + Toon(self.username, + self.password, + app[CONF_CLIENT_ID], + app[CONF_CLIENT_SECRET], + tenant_id=self.tenant, + display_common_name=user_input[CONF_DISPLAY]) + + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error while authenticating") + return self.async_abort(reason='unknown_auth_fail') + + return self.async_create_entry( + title=user_input[CONF_DISPLAY], + data={ + CONF_USERNAME: self.username, + CONF_PASSWORD: self.password, + CONF_TENANT: self.tenant, + CONF_DISPLAY: user_input[CONF_DISPLAY] + } + ) diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py new file mode 100644 index 00000000000..762374eb41c --- /dev/null +++ b/homeassistant/components/toon/const.py @@ -0,0 +1,21 @@ +"""Constants for the Toon integration.""" +DOMAIN = 'toon' + +DATA_TOON = 'toon' +DATA_TOON_CONFIG = 'toon_config' +DATA_TOON_CLIENT = 'toon_client' + +CONF_CLIENT_ID = 'client_id' +CONF_CLIENT_SECRET = 'client_secret' +CONF_DISPLAY = 'display' +CONF_TENANT = 'tenant' + +DEFAULT_MAX_TEMP = 30.0 +DEFAULT_MIN_TEMP = 6.0 + +CURRENCY_EUR = 'EUR' +POWER_WATT = 'Watt' +POWER_KWH = 'kWh' +RATIO_PERCENT = '%' +VOLUME_CM3 = 'CM3' +VOLUME_M3 = 'M3' diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index ebd25e02cde..2a5921b78eb 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -1,217 +1,189 @@ -"""Support for rebranded Quby thermostat as provided by Eneco.""" +"""Support for Toon sensors.""" +from datetime import timedelta import logging -import datetime -from homeassistant.helpers.entity import Entity -import homeassistant.components.toon as toon_main +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import (ToonEntity, ToonElectricityMeterDeviceEntity, + ToonGasMeterDeviceEntity, ToonSolarDeviceEntity, + ToonBoilerDeviceEntity) +from .const import (CURRENCY_EUR, DATA_TOON_CLIENT, DOMAIN, POWER_KWH, + POWER_WATT, VOLUME_CM3, VOLUME_M3, RATIO_PERCENT) + +DEPENDENCIES = ['toon'] _LOGGER = logging.getLogger(__name__) -STATE_ATTR_DEVICE_TYPE = 'device_type' -STATE_ATTR_LAST_CONNECTED_CHANGE = 'last_connected_change' +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=30) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Toon sensors.""" - _toon_main = hass.data[toon_main.TOON_HANDLE] +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, + async_add_entities) -> None: + """Set up Toon sensors based on a config entry.""" + toon = hass.data[DATA_TOON_CLIENT][entry.entry_id] - sensor_items = [] - sensor_items.extend([ - ToonSensor(hass, 'Power_current', 'power-plug', 'Watt'), - ToonSensor(hass, 'Power_today', 'power-plug', 'kWh'), - ]) + sensors = [ + ToonElectricityMeterDeviceSensor(toon, 'power', 'value', + "Current Power Usage", + 'mdi:power-plug', POWER_WATT), + ToonElectricityMeterDeviceSensor(toon, 'power', 'average', + "Average Power Usage", + 'mdi:power-plug', POWER_WATT), + ToonElectricityMeterDeviceSensor(toon, 'power', 'daily_value', + "Power Usage Today", + 'mdi:power-plug', POWER_KWH), + ToonElectricityMeterDeviceSensor(toon, 'power', 'daily_cost', + "Power Cost Today", + 'mdi:power-plug', CURRENCY_EUR), + ToonElectricityMeterDeviceSensor(toon, 'power', 'average_daily', + "Average Daily Power Usage", + 'mdi:power-plug', POWER_KWH), + ToonElectricityMeterDeviceSensor(toon, 'power', 'meter_reading', + "Power Meter Feed IN Tariff 1", + 'mdi:power-plug', POWER_KWH), + ToonElectricityMeterDeviceSensor(toon, 'power', 'meter_reading_low', + "Power Meter Feed IN Tariff 2", + 'mdi:power-plug', POWER_KWH), + ] - if _toon_main.gas: - sensor_items.extend([ - ToonSensor(hass, 'Gas_current', 'gas-cylinder', 'CM3'), - ToonSensor(hass, 'Gas_today', 'gas-cylinder', 'M3'), + if toon.gas: + sensors.extend([ + ToonGasMeterDeviceSensor(toon, 'gas', 'value', "Current Gas Usage", + 'mdi:gas-cylinder', VOLUME_CM3), + ToonGasMeterDeviceSensor(toon, 'gas', 'average', + "Average Gas Usage", 'mdi:gas-cylinder', + VOLUME_CM3), + ToonGasMeterDeviceSensor(toon, 'gas', 'daily_usage', + "Gas Usage Today", 'mdi:gas-cylinder', + VOLUME_M3), + ToonGasMeterDeviceSensor(toon, 'gas', 'average_daily', + "Average Daily Gas Usage", + 'mdi:gas-cylinder', VOLUME_M3), + ToonGasMeterDeviceSensor(toon, 'gas', 'meter_reading', "Gas Meter", + 'mdi:gas-cylinder', VOLUME_M3), + ToonGasMeterDeviceSensor(toon, 'gas', 'daily_cost', + "Gas Cost Today", 'mdi:gas-cylinder', + CURRENCY_EUR), ]) - for plug in _toon_main.toon.smartplugs: - sensor_items.extend([ - FibaroSensor(hass, '{}_current_power'.format(plug.name), - plug.name, 'power-socket-eu', 'Watt'), - FibaroSensor(hass, '{}_today_energy'.format(plug.name), - plug.name, 'power-socket-eu', 'kWh'), + if toon.solar: + sensors.extend([ + ToonSolarDeviceSensor(toon, 'solar', 'value', + "Current Solar Production", + 'mdi:solar-power', POWER_WATT), + ToonSolarDeviceSensor(toon, 'solar', 'maximum', + "Max Solar Production", 'mdi:solar-power', + POWER_WATT), + ToonSolarDeviceSensor(toon, 'solar', 'produced', + "Solar Production to Grid", + 'mdi:solar-power', POWER_WATT), + ToonSolarDeviceSensor(toon, 'solar', 'average_produced', + "Average Solar Production to Grid", + 'mdi:solar-power', POWER_WATT), + ToonElectricityMeterDeviceSensor(toon, 'solar', + 'meter_reading_produced', + "Power Meter Feed OUT Tariff 1", + 'mdi:solar-power', + POWER_KWH), + ToonElectricityMeterDeviceSensor(toon, 'solar', + 'meter_reading_low_produced', + "Power Meter Feed OUT Tariff 2", + 'mdi:solar-power', POWER_KWH), ]) - if _toon_main.toon.solar.produced or _toon_main.solar: - sensor_items.extend([ - SolarSensor(hass, 'Solar_maximum', 'kWh'), - SolarSensor(hass, 'Solar_produced', 'kWh'), - SolarSensor(hass, 'Solar_value', 'Watt'), - SolarSensor(hass, 'Solar_average_produced', 'kWh'), - SolarSensor(hass, 'Solar_meter_reading_low_produced', 'kWh'), - SolarSensor(hass, 'Solar_meter_reading_produced', 'kWh'), - SolarSensor(hass, 'Solar_daily_cost_produced', 'Euro'), + if toon.thermostat_info.have_ot_boiler: + sensors.extend([ + ToonBoilerDeviceSensor(toon, 'thermostat_info', + 'current_modulation_level', + "Boiler Modulation Level", + 'mdi:percent', + RATIO_PERCENT), ]) - for smokedetector in _toon_main.toon.smokedetectors: - sensor_items.append( - FibaroSmokeDetector( - hass, '{}_smoke_detector'.format(smokedetector.name), - smokedetector.device_uuid, 'alarm-bell', '%') - ) - - add_entities(sensor_items) + async_add_entities(sensors) -class ToonSensor(Entity): - """Representation of a Toon sensor.""" +class ToonSensor(ToonEntity): + """Defines a Toon sensor.""" - def __init__(self, hass, name, icon, unit_of_measurement): + def __init__(self, toon, section: str, measurement: str, + name: str, icon: str, unit_of_measurement: str) -> None: """Initialize the Toon sensor.""" - self._name = name self._state = None - self._icon = 'mdi:{}'.format(icon) self._unit_of_measurement = unit_of_measurement - self.thermos = hass.data[toon_main.TOON_HANDLE] + self.section = section + self.measurement = measurement + + super().__init__(toon, name, icon) @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the mdi icon of the sensor.""" - return self._icon + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return '_'.join([DOMAIN, self.toon.agreement.id, 'sensor', + self.section, self.measurement]) @property def state(self): """Return the state of the sensor.""" - return self.thermos.get_data(self.name.lower()) + return self._state @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return self._unit_of_measurement - def update(self): + async def async_update(self) -> None: """Get the latest data from the sensor.""" - self.thermos.update() + section = getattr(self.toon, self.section) + value = None + + if self.section == 'power' and self.measurement == 'daily_value': + value = round((float(section.daily_usage) + + float(section.daily_usage_low)) / 1000.0, 2) + + if value is None: + value = getattr(section, self.measurement) + + if self.section == 'power' and \ + self.measurement in ['meter_reading', 'meter_reading_low', + 'average_daily']: + value = round(float(value)/1000.0, 2) + + if self.section == 'solar' and \ + self.measurement in ['meter_reading_produced', + 'meter_reading_low_produced']: + value = float(value)/1000.0 + + if self.section == 'gas' and \ + self.measurement in ['average_daily', 'daily_usage', + 'meter_reading']: + value = round(float(value)/1000.0, 2) + + self._state = max(0, value) -class FibaroSensor(Entity): - """Representation of a Fibaro sensor.""" +class ToonElectricityMeterDeviceSensor(ToonSensor, + ToonElectricityMeterDeviceEntity): + """Defines a Eletricity Meter sensor.""" - def __init__(self, hass, name, plug_name, icon, unit_of_measurement): - """Initialize the Fibaro sensor.""" - self._name = name - self._plug_name = plug_name - self._state = None - self._icon = 'mdi:{}'.format(icon) - self._unit_of_measurement = unit_of_measurement - self.toon = hass.data[toon_main.TOON_HANDLE] - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the mdi icon of the sensor.""" - return self._icon - - @property - def state(self): - """Return the state of the sensor.""" - value = '_'.join(self.name.lower().split('_')[1:]) - return self.toon.get_data(value, self._plug_name) - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - - def update(self): - """Get the latest data from the sensor.""" - self.toon.update() + pass -class SolarSensor(Entity): - """Representation of a Solar sensor.""" +class ToonGasMeterDeviceSensor(ToonSensor, ToonGasMeterDeviceEntity): + """Defines a Gas Meter sensor.""" - def __init__(self, hass, name, unit_of_measurement): - """Initialize the Solar sensor.""" - self._name = name - self._state = None - self._icon = 'mdi:weather-sunny' - self._unit_of_measurement = unit_of_measurement - self.toon = hass.data[toon_main.TOON_HANDLE] - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the mdi icon of the sensor.""" - return self._icon - - @property - def state(self): - """Return the state of the sensor.""" - return self.toon.get_data(self.name.lower()) - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - - def update(self): - """Get the latest data from the sensor.""" - self.toon.update() + pass -class FibaroSmokeDetector(Entity): - """Representation of a Fibaro smoke detector.""" +class ToonSolarDeviceSensor(ToonSensor, ToonSolarDeviceEntity): + """Defines a Solar sensor.""" - def __init__(self, hass, name, uid, icon, unit_of_measurement): - """Initialize the Fibaro smoke sensor.""" - self._name = name - self._uid = uid - self._state = None - self._icon = 'mdi:{}'.format(icon) - self._unit_of_measurement = unit_of_measurement - self.toon = hass.data[toon_main.TOON_HANDLE] + pass - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property - def icon(self): - """Return the mdi icon of the sensor.""" - return self._icon +class ToonBoilerDeviceSensor(ToonSensor, ToonBoilerDeviceEntity): + """Defines a Boiler sensor.""" - @property - def state_attributes(self): - """Return the state attributes of the smoke detectors.""" - value = datetime.datetime.fromtimestamp( - int(self.toon.get_data('last_connected_change', self.name)) - ).strftime('%Y-%m-%d %H:%M:%S') - - return { - STATE_ATTR_DEVICE_TYPE: - self.toon.get_data('device_type', self.name), - STATE_ATTR_LAST_CONNECTED_CHANGE: value, - } - - @property - def state(self): - """Return the state of the sensor.""" - value = self.name.lower().split('_', 1)[1] - return self.toon.get_data(value, self.name) - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - - def update(self): - """Get the latest data from the sensor.""" - self.toon.update() + pass diff --git a/homeassistant/components/toon/strings.json b/homeassistant/components/toon/strings.json new file mode 100644 index 00000000000..80d71d4e421 --- /dev/null +++ b/homeassistant/components/toon/strings.json @@ -0,0 +1,34 @@ +{ + "config": { + "title": "Toon", + "step": { + "authenticate": { + "title": "Link your Toon account", + "description": "Authenticate with your Eneco Toon account (not the developer account).", + "data": { + "username": "Username", + "password": "Password", + "tenant": "Tenant" + } + }, + "display": { + "title": "Select display", + "description": "Select the Toon display to connect with.", + "data": { + "display": "Choose display" + } + } + }, + "error": { + "credentials": "The provided credentials are invalid.", + "display_exists": "The selected display is already configured." + }, + "abort": { + "client_id": "The client ID from the configuration is invalid.", + "client_secret": "The client secret from the configuration is invalid.", + "unknown_auth_fail": "Unexpected error occured, while authenticating.", + "no_agreements": "This account has no Toon displays.", + "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/)." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/switch.py b/homeassistant/components/toon/switch.py deleted file mode 100644 index 08ccec588b4..00000000000 --- a/homeassistant/components/toon/switch.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Support for Eneco Slimmer stekkers (Smart Plugs).""" -import logging - -from homeassistant.components.switch import SwitchDevice -import homeassistant.components.toon as toon_main - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the discovered Toon Smart Plugs.""" - _toon_main = hass.data[toon_main.TOON_HANDLE] - switch_items = [] - for plug in _toon_main.toon.smartplugs: - switch_items.append(EnecoSmartPlug(hass, plug)) - - add_entities(switch_items) - - -class EnecoSmartPlug(SwitchDevice): - """Representation of a Toon Smart Plug.""" - - def __init__(self, hass, plug): - """Initialize the Smart Plug.""" - self.smartplug = plug - self.toon_data_store = hass.data[toon_main.TOON_HANDLE] - - @property - def unique_id(self): - """Return the ID of this switch.""" - return self.smartplug.device_uuid - - @property - def name(self): - """Return the name of the switch if any.""" - return self.smartplug.name - - @property - def current_power_w(self): - """Return the current power usage in W.""" - return self.toon_data_store.get_data('current_power', self.name) - - @property - def today_energy_kwh(self): - """Return the today total energy usage in kWh.""" - return self.toon_data_store.get_data('today_energy', self.name) - - @property - def is_on(self): - """Return true if switch is on. Standby is on.""" - return self.toon_data_store.get_data('current_state', self.name) - - @property - def available(self): - """Return true if switch is available.""" - return self.smartplug.can_toggle - - def turn_on(self, **kwargs): - """Turn the switch on.""" - return self.smartplug.turn_on() - - def turn_off(self, **kwargs): - """Turn the switch off.""" - return self.smartplug.turn_off() - - def update(self): - """Update state.""" - self.toon_data_store.update() diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 34b30cf422a..7b22c2e197c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -172,6 +172,7 @@ FLOWS = [ 'smhi', 'sonos', 'tellduslive', + 'toon', 'tplink', 'tradfri', 'twilio', diff --git a/requirements_all.txt b/requirements_all.txt index 48a2e6990c7..e4899715228 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1685,7 +1685,7 @@ tikteck==0.4 todoist-python==7.0.17 # homeassistant.components.toon -toonlib==1.1.3 +toonapilib==3.0.9 # homeassistant.components.alarm_control_panel.totalconnect total_connect_client==0.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba283d1d2c6..0093aa87cac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -290,6 +290,9 @@ srpenergy==1.0.5 # homeassistant.components.statsd statsd==3.2.1 +# homeassistant.components.toon +toonapilib==3.0.9 + # homeassistant.components.camera.uvc uvcclient==0.11.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 24d081349a0..1355b71b7b1 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -118,6 +118,7 @@ TEST_REQUIREMENTS = ( 'sqlalchemy', 'srpenergy', 'statsd', + 'toonapilib', 'uvcclient', 'vsure', 'warrant', diff --git a/tests/components/toon/__init__.py b/tests/components/toon/__init__.py new file mode 100644 index 00000000000..96de853baff --- /dev/null +++ b/tests/components/toon/__init__.py @@ -0,0 +1 @@ +"""Tests for the Toon component.""" diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py new file mode 100644 index 00000000000..44cb54fc98e --- /dev/null +++ b/tests/components/toon/test_config_flow.py @@ -0,0 +1,177 @@ +"""Tests for the Toon config flow.""" + +from unittest.mock import patch + +import pytest +from toonapilib.toonapilibexceptions import ( + AgreementsRetrievalError, InvalidConsumerKey, InvalidConsumerSecret, + InvalidCredentials) + +from homeassistant import data_entry_flow +from homeassistant.components.toon import config_flow +from homeassistant.components.toon.const import ( + CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, DOMAIN) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, MockDependency + +FIXTURE_APP = { + DOMAIN: { + CONF_CLIENT_ID: '1234567890abcdef', + CONF_CLIENT_SECRET: '1234567890abcdef', + } +} + +FIXTURE_CREDENTIALS = { + CONF_USERNAME: 'john.doe', + CONF_PASSWORD: 'secret', + CONF_TENANT: 'eneco' +} + +FIXTURE_DISPLAY = { + CONF_DISPLAY: 'display1' +} + + +@pytest.fixture +def mock_toonapilib(): + """Mock toonapilib.""" + with MockDependency('toonapilib') as mock_toonapilib_: + mock_toonapilib_.Toon().display_names = [FIXTURE_DISPLAY[CONF_DISPLAY]] + yield mock_toonapilib_ + + +async def setup_component(hass): + """Set up Toon component.""" + with patch('os.path.isfile', return_value=False): + assert await async_setup_component(hass, DOMAIN, FIXTURE_APP) + await hass.async_block_till_done() + + +async def test_abort_if_no_app_configured(hass): + """Test abort if no app is configured.""" + flow = config_flow.ToonFlowHandler() + flow.hass = hass + result = await flow.async_step_user() + + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'no_app' + + +async def test_show_authenticate_form(hass): + """Test that the authentication form is served.""" + await setup_component(hass) + + flow = config_flow.ToonFlowHandler() + flow.hass = hass + result = await flow.async_step_user(user_input=None) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'authenticate' + + +@pytest.mark.parametrize('side_effect,reason', + [(InvalidConsumerKey, 'client_id'), + (InvalidConsumerSecret, 'client_secret'), + (AgreementsRetrievalError, 'no_agreements'), + (Exception, 'unknown_auth_fail')]) +async def test_toon_abort(hass, mock_toonapilib, side_effect, reason): + """Test we abort on Toon error.""" + await setup_component(hass) + + flow = config_flow.ToonFlowHandler() + flow.hass = hass + + mock_toonapilib.Toon.side_effect = side_effect + + result = await flow.async_step_authenticate(user_input=FIXTURE_CREDENTIALS) + + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == reason + + +async def test_invalid_credentials(hass, mock_toonapilib): + """Test we show authentication form on Toon auth error.""" + mock_toonapilib.Toon.side_effect = InvalidCredentials + + await setup_component(hass) + + flow = config_flow.ToonFlowHandler() + flow.hass = hass + result = await flow.async_step_user(user_input=FIXTURE_CREDENTIALS) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'authenticate' + assert result['errors'] == {'base': 'credentials'} + + +async def test_full_flow_implementation(hass, mock_toonapilib): + """Test registering an integration and finishing flow works.""" + await setup_component(hass) + + flow = config_flow.ToonFlowHandler() + flow.hass = hass + result = await flow.async_step_user(user_input=None) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'authenticate' + + result = await flow.async_step_user(user_input=FIXTURE_CREDENTIALS) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'display' + + result = await flow.async_step_display(user_input=FIXTURE_DISPLAY) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == FIXTURE_DISPLAY[CONF_DISPLAY] + assert result['data'][CONF_USERNAME] == FIXTURE_CREDENTIALS[CONF_USERNAME] + assert result['data'][CONF_PASSWORD] == FIXTURE_CREDENTIALS[CONF_PASSWORD] + assert result['data'][CONF_TENANT] == FIXTURE_CREDENTIALS[CONF_TENANT] + assert result['data'][CONF_DISPLAY] == FIXTURE_DISPLAY[CONF_DISPLAY] + + +async def test_no_displays(hass, mock_toonapilib): + """Test abort when there are no displays.""" + await setup_component(hass) + + mock_toonapilib.Toon().display_names = [] + + flow = config_flow.ToonFlowHandler() + flow.hass = hass + await flow.async_step_user(user_input=FIXTURE_CREDENTIALS) + + result = await flow.async_step_display(user_input=None) + + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'no_displays' + + +async def test_display_already_exists(hass, mock_toonapilib): + """Test showing display form again if display already exists.""" + await setup_component(hass) + + flow = config_flow.ToonFlowHandler() + flow.hass = hass + await flow.async_step_user(user_input=FIXTURE_CREDENTIALS) + + MockConfigEntry(domain=DOMAIN, data=FIXTURE_DISPLAY).add_to_hass(hass) + + result = await flow.async_step_display(user_input=FIXTURE_DISPLAY) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'display' + assert result['errors'] == {'base': 'display_exists'} + + +async def test_abort_last_minute_fail(hass, mock_toonapilib): + """Test we abort when API communication fails in the last step.""" + await setup_component(hass) + + flow = config_flow.ToonFlowHandler() + flow.hass = hass + await flow.async_step_user(user_input=FIXTURE_CREDENTIALS) + + mock_toonapilib.Toon.side_effect = Exception + + result = await flow.async_step_display(user_input=FIXTURE_DISPLAY) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'unknown_auth_fail' From 90d3f517d8ca08807ef731439bb5271f852a507c Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 26 Feb 2019 20:20:54 +0200 Subject: [PATCH 169/253] Check if a script requirement is available before install (#20517) * Check if a script requirement is available before install * PackageLoadable * hound * req --- homeassistant/scripts/__init__.py | 22 ++++++++++++++++------ homeassistant/scripts/check_config.py | 3 --- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 02cc0bff362..3050379a496 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -9,7 +9,8 @@ from typing import List from homeassistant.bootstrap import async_mount_local_lib_path from homeassistant.config import get_default_config_dir -from homeassistant import requirements +from homeassistant.core import HomeAssistant +from homeassistant.requirements import pip_kwargs, PackageLoadable from homeassistant.util.package import install_package, is_virtual_env @@ -39,16 +40,25 @@ def run(args: List) -> int: config_dir = extract_config_dir() - if not is_virtual_env(): - asyncio.get_event_loop().run_until_complete( - async_mount_local_lib_path(config_dir)) + loop = asyncio.get_event_loop() - pip_kwargs = requirements.pip_kwargs(config_dir) + if not is_virtual_env(): + loop.run_until_complete(async_mount_local_lib_path(config_dir)) + + _pip_kwargs = pip_kwargs(config_dir) logging.basicConfig(stream=sys.stdout, level=logging.INFO) + hass = HomeAssistant(loop) + pkgload = PackageLoadable(hass) for req in getattr(script, 'REQUIREMENTS', []): - returncode = install_package(req, **pip_kwargs) + try: + loop.run_until_complete(pkgload.loadable(req)) + continue + except ImportError: + pass + + returncode = install_package(req, **_pip_kwargs) if not returncode: print('Aborting script, could not install dependency', req) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 67bc97da992..1b8c6719395 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -5,7 +5,6 @@ import logging import os from collections import OrderedDict, namedtuple from glob import glob -from platform import system from typing import Dict, List, Sequence from unittest.mock import patch @@ -22,8 +21,6 @@ from homeassistant.util import yaml from homeassistant.exceptions import HomeAssistantError REQUIREMENTS = ('colorlog==4.0.2',) -if system() == 'Windows': # Ensure colorama installed for colorlog on Windows - REQUIREMENTS += ('colorama<=1',) _LOGGER = logging.getLogger(__name__) # pylint: disable=protected-access From 5a5c97acb320710f3a59a23f00ae2c81fcfc5b60 Mon Sep 17 00:00:00 2001 From: Giorgos Logiotatidis Date: Tue, 26 Feb 2019 20:23:46 +0200 Subject: [PATCH 170/253] Add timeout option to sensor.rest and binary_sensor.rest. (#20065) --- homeassistant/components/binary_sensor/rest.py | 8 ++++++-- homeassistant/components/sensor/rest.py | 15 +++++++++++---- tests/components/sensor/test_rest.py | 6 +++++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index ac82ab126fd..304ed701148 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.sensor.rest import RestData from homeassistant.const import ( CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE, - CONF_VERIFY_SSL, CONF_USERNAME, CONF_PASSWORD, + CONF_VERIFY_SSL, CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, CONF_DEVICE_CLASS) import homeassistant.helpers.config_validation as cv @@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_METHOD = 'GET' DEFAULT_NAME = 'REST Binary Sensor' DEFAULT_VERIFY_SSL = True +DEFAULT_TIMEOUT = 10 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.url, @@ -39,6 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, }) @@ -49,6 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): method = config.get(CONF_METHOD) payload = config.get(CONF_PAYLOAD) verify_ssl = config.get(CONF_VERIFY_SSL) + timeout = config.get(CONF_TIMEOUT) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) headers = config.get(CONF_HEADERS) @@ -65,7 +68,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: auth = None - rest = RestData(method, resource, auth, headers, payload, verify_ssl) + rest = RestData(method, resource, auth, headers, payload, verify_ssl, + timeout) rest.update() if rest.data is None: raise PlatformNotReady diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 4eb4b940095..a9446ee3503 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -16,7 +16,7 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( CONF_AUTHENTICATION, CONF_FORCE_UPDATE, CONF_HEADERS, CONF_NAME, CONF_METHOD, CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE, - CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, + CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_TIMEOUT, CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, CONF_DEVICE_CLASS, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) from homeassistant.exceptions import PlatformNotReady @@ -29,6 +29,7 @@ DEFAULT_METHOD = 'GET' DEFAULT_NAME = 'REST Sensor' DEFAULT_VERIFY_SSL = True DEFAULT_FORCE_UPDATE = False +DEFAULT_TIMEOUT = 10 CONF_JSON_ATTRS = 'json_attributes' METHODS = ['POST', 'GET'] @@ -49,6 +50,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, }) @@ -67,6 +69,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): value_template = config.get(CONF_VALUE_TEMPLATE) json_attrs = config.get(CONF_JSON_ATTRS) force_update = config.get(CONF_FORCE_UPDATE) + timeout = config.get(CONF_TIMEOUT) if value_template is not None: value_template.hass = hass @@ -78,7 +81,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): auth = HTTPBasicAuth(username, password) else: auth = None - rest = RestData(method, resource, auth, headers, payload, verify_ssl) + rest = RestData(method, resource, auth, headers, payload, verify_ssl, + timeout) rest.update() if rest.data is None: raise PlatformNotReady @@ -174,11 +178,13 @@ class RestSensor(Entity): class RestData: """Class for handling the data retrieval.""" - def __init__(self, method, resource, auth, headers, data, verify_ssl): + def __init__(self, method, resource, auth, headers, data, verify_ssl, + timeout=DEFAULT_TIMEOUT): """Initialize the data object.""" self._request = requests.Request( method, resource, headers=headers, auth=auth, data=data).prepare() self._verify_ssl = verify_ssl + self._timeout = timeout self.data = None def update(self): @@ -187,7 +193,8 @@ class RestData: try: with requests.Session() as sess: response = sess.send( - self._request, timeout=10, verify=self._verify_ssl) + self._request, timeout=self._timeout, + verify=self._verify_ssl) self.data = response.text except requests.exceptions.RequestException as ex: diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 3e71be8a6f6..343cc696763 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -89,6 +89,7 @@ class TestRestSensorSetup(unittest.TestCase): 'name': 'foo', 'unit_of_measurement': 'MB', 'verify_ssl': 'true', + 'timeout': 30, 'authentication': 'basic', 'username': 'my username', 'password': 'my password', @@ -112,6 +113,7 @@ class TestRestSensorSetup(unittest.TestCase): 'name': 'foo', 'unit_of_measurement': 'MB', 'verify_ssl': 'true', + 'timeout': 30, 'authentication': 'basic', 'username': 'my username', 'password': 'my password', @@ -280,8 +282,10 @@ class TestRestData(unittest.TestCase): self.method = "GET" self.resource = "http://localhost" self.verify_ssl = True + self.timeout = 10 self.rest = rest.RestData( - self.method, self.resource, None, None, None, self.verify_ssl) + self.method, self.resource, None, None, None, self.verify_ssl, + self.timeout) @requests_mock.Mocker() def test_update(self, mock_req): From 03fc81a4348b2bbd5898fe65fc362c733bbaf24b Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 26 Feb 2019 19:47:08 +0100 Subject: [PATCH 171/253] Clean up codeowners file (#21442) --- CODEOWNERS | 68 +++++++++++++++--------------------------------------- 1 file changed, 18 insertions(+), 50 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9d0476e7a37..ac8f98a11b0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,7 +28,7 @@ homeassistant/components/panel_iframe/* @home-assistant/core homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/scene/__init__.py @home-assistant/core -homeassistant/components/scene/hass.py @home-assistant/core +homeassistant/components/scene/homeassistant.py @home-assistant/core homeassistant/components/script/* @home-assistant/core homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/sun/* @home-assistant/core @@ -47,7 +47,6 @@ homeassistant/components/*/zwave.py @home-assistant/z-wave homeassistant/components/hassio/* @home-assistant/hassio # Individual platforms -homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell homeassistant/components/binary_sensor/hikvision.py @mezz64 homeassistant/components/binary_sensor/threshold.py @fabaff @@ -69,10 +68,7 @@ homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/device_tracker/traccar.py @ludeeus homeassistant/components/device_tracker/bt_smarthub.py @jxwolstenholme homeassistant/components/device_tracker/synology_srm.py @aerialls -homeassistant/components/history_graph/* @andrey-git -homeassistant/components/influx/* @fabaff homeassistant/components/light/lifx_legacy.py @amelchio -homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti homeassistant/components/light/yeelightsunflower.py @lindsaymarkward homeassistant/components/lock/nello.py @pschmitt @@ -83,20 +79,15 @@ homeassistant/components/media_player/liveboxplaytv.py @pschmitt homeassistant/components/media_player/mediaroom.py @dgomes homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/media_player/mpd.py @fabaff -homeassistant/components/media_player/sonos.py @amelchio homeassistant/components/media_player/xiaomi_tv.py @fattdev homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth -homeassistant/components/no_ip/* @fabaff homeassistant/components/notify/file.py @fabaff homeassistant/components/notify/flock.py @fabaff -homeassistant/components/notify/instapush.py @fabaff homeassistant/components/notify/mastodon.py @fabaff homeassistant/components/notify/smtp.py @fabaff homeassistant/components/notify/syslog.py @fabaff homeassistant/components/notify/xmpp.py @fabaff homeassistant/components/notify/yessssms.py @flowolf -homeassistant/components/plant/* @ChristianKuehnel -homeassistant/components/remote/harmony.py @ehendrix23 homeassistant/components/scene/lifx_cloud.py @amelchio homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/alpha_vantage.py @fabaff @@ -107,7 +98,7 @@ homeassistant/components/sensor/darksky.py @fabaff homeassistant/components/sensor/file.py @fabaff homeassistant/components/sensor/filter.py @dgomes homeassistant/components/sensor/fixer.py @fabaff -homeassistant/components/sensor/flunearyou.py.py @bachya +homeassistant/components/sensor/flunearyou.py @bachya homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/gitter.py @fabaff homeassistant/components/sensor/glances.py @fabaff @@ -137,35 +128,28 @@ homeassistant/components/sensor/statistics.py @fabaff homeassistant/components/sensor/swiss*.py @fabaff homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/tautulli.py @ludeeus -homeassistant/components/sensor/time_data.py @fabaff +homeassistant/components/sensor/time_date.py @fabaff homeassistant/components/sensor/version.py @fabaff homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/sensor/worldclock.py @fabaff -homeassistant/components/shiftr/* @fabaff -homeassistant/components/spaceapi/* @fabaff homeassistant/components/switch/switchbot.py @danielhiversen homeassistant/components/switch/switchmate.py @danielhiversen -homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/weather/__init__.py @fabaff homeassistant/components/weather/darksky.py @fabaff homeassistant/components/weather/demo.py @fabaff homeassistant/components/weather/met.py @danielhiversen homeassistant/components/weather/openweathermap.py @fabaff -homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi # A homeassistant/components/ambient_station/* @bachya homeassistant/components/arduino/* @fabaff -homeassistant/components/*/arduino.py @fabaff +homeassistant/components/axis/* @kane610 homeassistant/components/*/arest.py @fabaff -homeassistant/components/*/axis.py @kane610 # B homeassistant/components/blink/* @fronzbot -homeassistant/components/*/blink.py @fronzbot homeassistant/components/bmw_connected_drive/* @ChristianKuehnel -homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/broadlink.py @danielhiversen # C @@ -174,20 +158,15 @@ homeassistant/components/counter/* @fabaff # D homeassistant/components/daikin/* @fredrike @rofrantz -homeassistant/components/*/daikin.py @fredrike @rofrantz -homeassistant/components/*/deconz.py @kane610 +homeassistant/components/deconz/* @kane610 homeassistant/components/digital_ocean/* @fabaff -homeassistant/components/*/digital_ocean.py @fabaff homeassistant/components/dweet/* @fabaff -homeassistant/components/*/dweet.py @fabaff # E homeassistant/components/ecovacs/* @OverloadUT -homeassistant/components/*/ecovacs.py @OverloadUT -homeassistant/components/*/edp_redy.py @abmantis homeassistant/components/edp_redy/* @abmantis homeassistant/components/eight_sleep/* @mezz64 -homeassistant/components/*/eight_sleep.py @mezz64 +homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/esphome/*.py @OttoWinter # F @@ -195,33 +174,28 @@ homeassistant/components/freebox/*.py @snoof85 # G homeassistant/components/googlehome/* @ludeeus -homeassistant/components/*/googlehome.py @ludeeus # H +homeassistant/components/harmony/* @ehendrix23 +homeassistant/components/history_graph/* @andrey-git homeassistant/components/hive/* @Rendili @KJonline -homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p homeassistant/components/huawei_lte/* @scop -homeassistant/components/*/huawei_lte.py @scop # I +homeassistant/components/influx/* @fabaff homeassistant/components/ipma/* @dgomes # K homeassistant/components/knx/* @Julius2342 -homeassistant/components/*/knx.py @Julius2342 homeassistant/components/konnected/* @heythisisnate -homeassistant/components/*/konnected.py @heythisisnate # L homeassistant/components/lifx/* @amelchio -homeassistant/components/*/lifx.py @amelchio homeassistant/components/luftdaten/* @fabaff -homeassistant/components/*/luftdaten.py @fabaff # M homeassistant/components/matrix/* @tinloaf -homeassistant/components/*/matrix.py @tinloaf homeassistant/components/melissa/* @kennedyshead homeassistant/components/*/melissa.py @kennedyshead homeassistant/components/*/mystrom.py @fabaff @@ -230,61 +204,55 @@ homeassistant/components/*/mystrom.py @fabaff homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/*/ness_alarm.py @nickw444 homeassistant/components/nissan_leaf/* @filcole +homeassistant/components/no_ip/* @fabaff # O homeassistant/components/openuv/* @bachya # P +homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/point/* @fredrike -homeassistant/components/*/point.py @fredrike # Q homeassistant/components/qwikswitch/* @kellerza -homeassistant/components/*/qwikswitch.py @kellerza # R homeassistant/components/rainmachine/* @bachya +homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/*/random.py @fabaff -homeassistant/components/*/rfxtrx.py @danielhiversen # S +homeassistant/components/shiftr/* @fabaff homeassistant/components/simplisafe/* @bachya homeassistant/components/smartthings/* @andrewsayre +homeassistant/components/sonos/* @amelchio +homeassistant/components/spaceapi/* @fabaff homeassistant/components/spider/* @peternijssen # T homeassistant/components/tahoma/* @philklei -homeassistant/components/*/tahoma.py @philklei homeassistant/components/tellduslive/*.py @fredrike -homeassistant/components/*/tellduslive.py @fredrike homeassistant/components/tesla/* @zabuldon -homeassistant/components/*/tesla.py @zabuldon homeassistant/components/thethingsnetwork/* @fabaff -homeassistant/components/*/thethingsnetwork.py @fabaff homeassistant/components/tibber/* @danielhiversen -homeassistant/components/*/tibber.py @danielhiversen +homeassistant/components/tplink/* @rytilahti homeassistant/components/tradfri/* @ggravlingen -homeassistant/components/*/tradfri.py @ggravlingen homeassistant/components/toon/* @frenck # U homeassistant/components/unifi/* @kane610 -homeassistant/components/switch/unifi.py @kane610 homeassistant/components/upcloud/* @scop homeassistant/components/utility_meter/* @dgomes -homeassistant/components/*/upcloud.py @scop # V homeassistant/components/velux/* @Julius2342 -homeassistant/components/*/velux.py @Julius2342 # W homeassistant/components/wemo/* @sqldiablo -homeassistant/components/*/wemo.py @sqldiablo # X -homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi -homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi +homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi +homeassistant/components/xiaomi_miio/* @rytilahti @syssi # Z homeassistant/components/zoneminder/* @rohankapoorcom From beb86426e44588fb26a161ba1a2f751e7309d169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 26 Feb 2019 20:47:57 +0200 Subject: [PATCH 172/253] Upgrade flake8 to 3.7.7 (#21452) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 1a862f2d406..6568ca89a07 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,7 +4,7 @@ asynctest==0.12.2 coveralls==1.2.0 flake8-docstrings==1.3.0 -flake8==3.7.5 +flake8==3.7.7 mock-open==1.3.1 mypy==0.670 pydocstyle==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0093aa87cac..307c283e8e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -5,7 +5,7 @@ asynctest==0.12.2 coveralls==1.2.0 flake8-docstrings==1.3.0 -flake8==3.7.5 +flake8==3.7.7 mock-open==1.3.1 mypy==0.670 pydocstyle==3.0.0 From a34524febe12bb2f3ae4520cabf553d42c58b5fd Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 26 Feb 2019 13:48:10 -0500 Subject: [PATCH 173/253] Clean up ZHA post rewrite (#21448) * update async handling to reduce unnecessary coroutine creation * lint * cleanup --- homeassistant/components/zha/__init__.py | 8 +- homeassistant/components/zha/api.py | 8 +- .../components/zha/core/channels/__init__.py | 11 +- .../components/zha/core/channels/lighting.py | 8 ++ homeassistant/components/zha/core/device.py | 41 +++--- homeassistant/components/zha/core/gateway.py | 125 +++++++++--------- 6 files changed, 110 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index a53e5864552..96c3a30d313 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -154,11 +154,9 @@ async def async_setup_entry(hass, config_entry): """Handle message from a device.""" if not sender.initializing and sender.ieee in zha_gateway.devices and \ not zha_gateway.devices[sender.ieee].available: - hass.async_create_task( - zha_gateway.async_device_became_available( - sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, - command_id, args - ) + zha_gateway.async_device_became_available( + sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, + command_id, args ) return sender.handle_message( is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 0dd6dd78400..f0739f9a073 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -251,7 +251,7 @@ def async_load_api(hass, application_controller, zha_gateway): zha_device = zha_gateway.get_device(ieee) response_clusters = [] if zha_device is not None: - clusters_by_endpoint = await zha_device.get_clusters() + clusters_by_endpoint = zha_device.async_get_clusters() for ep_id, clusters in clusters_by_endpoint.items(): for c_id, cluster in clusters[IN].items(): response_clusters.append({ @@ -289,7 +289,7 @@ def async_load_api(hass, application_controller, zha_gateway): zha_device = zha_gateway.get_device(ieee) attributes = None if zha_device is not None: - attributes = await zha_device.get_cluster_attributes( + attributes = zha_device.async_get_cluster_attributes( endpoint_id, cluster_id, cluster_type) @@ -329,7 +329,7 @@ def async_load_api(hass, application_controller, zha_gateway): cluster_commands = [] commands = None if zha_device is not None: - commands = await zha_device.get_cluster_commands( + commands = zha_device.async_get_cluster_commands( endpoint_id, cluster_id, cluster_type) @@ -380,7 +380,7 @@ def async_load_api(hass, application_controller, zha_gateway): zha_device = zha_gateway.get_device(ieee) success = failure = None if zha_device is not None: - cluster = await zha_device.get_cluster( + cluster = zha_device.async_get_cluster( endpoint_id, cluster_id, cluster_type=cluster_type) success, failure = await cluster.read_attributes( [attribute], diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 0c0e1ed2173..a070343b775 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import asyncio +from concurrent.futures import TimeoutError as Timeout from enum import Enum from functools import wraps import logging @@ -55,9 +56,13 @@ def decorate_command(channel, command): if isinstance(result, bool): return result return result[1] is Status.SUCCESS - except DeliveryError: - _LOGGER.debug("%s: command failed: %s", channel.unique_id, - command.__name__) + except (DeliveryError, Timeout) as ex: + _LOGGER.debug( + "%s: command failed: %s exception: %s", + channel.unique_id, + command.__name__, + str(ex) + ) return False return wrapper diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index ee88a30e828..9c904a7a001 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -28,8 +28,16 @@ class ColorChannel(ZigbeeChannel): """Return the color capabilities.""" return self._color_capabilities + async def async_configure(self): + """Configure channel.""" + await self.fetch_color_capabilities(False) + async def async_initialize(self, from_cache): """Initialize channel.""" + await self.fetch_color_capabilities(True) + + async def fetch_color_capabilities(self, from_cache): + """Get the color configuration.""" capabilities = await self.get_attribute_value( 'color_capabilities', from_cache=from_cache) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 1ee800d8559..102c9bed2d3 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -8,6 +8,7 @@ import asyncio from enum import Enum import logging +from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send ) @@ -188,13 +189,14 @@ class ZHADevice: """Initialize channels.""" _LOGGER.debug('%s: started initialization', self.name) await self._execute_channel_tasks('async_initialize', from_cache) - self.power_source = self.cluster_channels.get( - BASIC_CHANNEL).get_power_source() - _LOGGER.debug( - '%s: power source: %s', - self.name, - BasicChannel.POWER_SOURCES.get(self.power_source) - ) + if BASIC_CHANNEL in self.cluster_channels: + self.power_source = self.cluster_channels.get( + BASIC_CHANNEL).get_power_source() + _LOGGER.debug( + '%s: power source: %s', + self.name, + BasicChannel.POWER_SOURCES.get(self.power_source) + ) self.status = DeviceStatus.INITIALIZED _LOGGER.debug('%s: completed initialization', self.name) @@ -229,7 +231,8 @@ class ZHADevice: if self._unsub: self._unsub() - async def get_clusters(self): + @callback + def async_get_clusters(self): """Get all clusters for this device.""" return { ep_id: { @@ -239,25 +242,27 @@ class ZHADevice: if ep_id != 0 } - async def get_cluster(self, endpoint_id, cluster_id, cluster_type=IN): + @callback + def async_get_cluster(self, endpoint_id, cluster_id, cluster_type=IN): """Get zigbee cluster from this entity.""" - clusters = await self.get_clusters() + clusters = self.async_get_clusters() return clusters[endpoint_id][cluster_type][cluster_id] - async def get_cluster_attributes(self, endpoint_id, cluster_id, + @callback + def async_get_cluster_attributes(self, endpoint_id, cluster_id, cluster_type=IN): """Get zigbee attributes for specified cluster.""" - cluster = await self.get_cluster(endpoint_id, cluster_id, + cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None return cluster.attributes - async def get_cluster_commands(self, endpoint_id, cluster_id, + @callback + def async_get_cluster_commands(self, endpoint_id, cluster_id, cluster_type=IN): """Get zigbee commands for specified cluster.""" - cluster = await self.get_cluster(endpoint_id, cluster_id, - cluster_type) + cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None return { @@ -269,8 +274,7 @@ class ZHADevice: attribute, value, cluster_type=IN, manufacturer=None): """Write a value to a zigbee attribute for a cluster in this entity.""" - cluster = await self.get_cluster( - endpoint_id, cluster_id, cluster_type) + cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None @@ -304,8 +308,7 @@ class ZHADevice: command_type, args, cluster_type=IN, manufacturer=None): """Issue a command against specified zigbee cluster on this entity.""" - cluster = await self.get_cluster( - endpoint_id, cluster_id, cluster_type) + cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None response = None diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index cb5e5bf7774..563543fa4bd 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -5,11 +5,11 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ -import asyncio import collections import itertools import logging from homeassistant import const as ha_const +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_component import EntityComponent from . import const as zha_const @@ -122,7 +122,8 @@ class ZHAGateway: ) ) - async def _get_or_create_device(self, zigpy_device): + @callback + def _async_get_or_create_device(self, zigpy_device): """Get or create a ZHA device.""" zha_device = self._devices.get(zigpy_device.ieee) if zha_device is None: @@ -130,12 +131,14 @@ class ZHAGateway: self._devices[zigpy_device.ieee] = zha_device return zha_device - async def async_device_became_available( + @callback + def async_device_became_available( self, sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args): """Handle tasks when a device becomes available.""" self.async_update_device(sender) + @callback def async_update_device(self, sender): """Update device that has just become available.""" if sender.ieee in self.devices: @@ -146,34 +149,17 @@ class ZHAGateway: async def async_device_initialized(self, device, is_new_join): """Handle device joined and basic information discovered (async).""" - zha_device = await self._get_or_create_device(device) + zha_device = self._async_get_or_create_device(device) discovery_infos = [] - endpoint_tasks = [] for endpoint_id, endpoint in device.endpoints.items(): - endpoint_tasks.append(self._async_process_endpoint( + self._async_process_endpoint( endpoint_id, endpoint, discovery_infos, device, zha_device, is_new_join - )) - await asyncio.gather(*endpoint_tasks) - - await zha_device.async_initialize(from_cache=(not is_new_join)) - - discovery_tasks = [] - for discovery_info in discovery_infos: - discovery_tasks.append(_dispatch_discovery_info( - self._hass, - is_new_join, - discovery_info - )) - await asyncio.gather(*discovery_tasks) - - device_entity = _create_device_entity(zha_device) - await self._component.async_add_entities([device_entity]) + ) if is_new_join: - # because it's a new join we can immediately mark the device as - # available and we already loaded fresh state above - zha_device.update_available(True) + # configure the device + await zha_device.async_configure() elif not zha_device.available and zha_device.power_source is not None\ and zha_device.power_source != BasicChannel.BATTERY\ and zha_device.power_source != BasicChannel.UNKNOWN: @@ -187,15 +173,33 @@ class ZHAGateway: ) ) await zha_device.async_initialize(from_cache=False) + else: + await zha_device.async_initialize(from_cache=True) - async def _async_process_endpoint( + for discovery_info in discovery_infos: + _async_dispatch_discovery_info( + self._hass, + is_new_join, + discovery_info + ) + + device_entity = _async_create_device_entity(zha_device) + await self._component.async_add_entities([device_entity]) + + if is_new_join: + # because it's a new join we can immediately mark the device as + # available. We do it here because the entities didn't exist above + zha_device.update_available(True) + + @callback + def _async_process_endpoint( self, endpoint_id, endpoint, discovery_infos, device, zha_device, is_new_join): """Process an endpoint on a zigpy device.""" import zigpy.profiles if endpoint_id == 0: # ZDO - await _create_cluster_channel( + _async_create_cluster_channel( endpoint, zha_device, is_new_join, @@ -226,12 +230,12 @@ class ZHAGateway: profile_clusters = zha_const.COMPONENT_CLUSTERS[component] if component and component in COMPONENTS: - profile_match = await _handle_profile_match( + profile_match = _async_handle_profile_match( self._hass, endpoint, profile_clusters, zha_device, component, device_key, is_new_join) discovery_infos.append(profile_match) - discovery_infos.extend(await _handle_single_cluster_matches( + discovery_infos.extend(_async_handle_single_cluster_matches( self._hass, endpoint, zha_device, @@ -241,21 +245,21 @@ class ZHAGateway: )) -async def _create_cluster_channel(cluster, zha_device, is_new_join, +@callback +def _async_create_cluster_channel(cluster, zha_device, is_new_join, channels=None, channel_class=None): """Create a cluster channel and attach it to a device.""" if channel_class is None: channel_class = ZIGBEE_CHANNEL_REGISTRY.get(cluster.cluster_id, AttributeListeningChannel) channel = channel_class(cluster, zha_device) - if is_new_join: - await channel.async_configure() zha_device.add_cluster_channel(channel) if channels is not None: channels.append(channel) -async def _dispatch_discovery_info(hass, is_new_join, discovery_info): +@callback +def _async_dispatch_discovery_info(hass, is_new_join, discovery_info): """Dispatch or store discovery information.""" if not discovery_info['channels']: _LOGGER.warning( @@ -273,7 +277,8 @@ async def _dispatch_discovery_info(hass, is_new_join, discovery_info): discovery_info -async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device, +@callback +def _async_handle_profile_match(hass, endpoint, profile_clusters, zha_device, component, device_key, is_new_join): """Dispatch a profile match to the appropriate HA component.""" in_clusters = [endpoint.in_clusters[c] @@ -284,17 +289,14 @@ async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device, if c in endpoint.out_clusters] channels = [] - cluster_tasks = [] for cluster in in_clusters: - cluster_tasks.append(_create_cluster_channel( - cluster, zha_device, is_new_join, channels=channels)) + _async_create_cluster_channel( + cluster, zha_device, is_new_join, channels=channels) for cluster in out_clusters: - cluster_tasks.append(_create_cluster_channel( - cluster, zha_device, is_new_join, channels=channels)) - - await asyncio.gather(*cluster_tasks) + _async_create_cluster_channel( + cluster, zha_device, is_new_join, channels=channels) discovery_info = { 'unique_id': device_key, @@ -319,24 +321,25 @@ async def _handle_profile_match(hass, endpoint, profile_clusters, zha_device, return discovery_info -async def _handle_single_cluster_matches(hass, endpoint, zha_device, +@callback +def _async_handle_single_cluster_matches(hass, endpoint, zha_device, profile_clusters, device_key, is_new_join): """Dispatch single cluster matches to HA components.""" cluster_matches = [] - cluster_match_tasks = [] - event_channel_tasks = [] + cluster_match_results = [] for cluster in endpoint.in_clusters.values(): # don't let profiles prevent these channels from being created if cluster.cluster_id in NO_SENSOR_CLUSTERS: - cluster_match_tasks.append(_handle_channel_only_cluster_match( - zha_device, - cluster, - is_new_join, - )) + cluster_match_results.append( + _async_handle_channel_only_cluster_match( + zha_device, + cluster, + is_new_join, + )) if cluster.cluster_id not in profile_clusters[0]: - cluster_match_tasks.append(_handle_single_cluster_match( + cluster_match_results.append(_async_handle_single_cluster_match( hass, zha_device, cluster, @@ -347,7 +350,7 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, for cluster in endpoint.out_clusters.values(): if cluster.cluster_id not in profile_clusters[1]: - cluster_match_tasks.append(_handle_single_cluster_match( + cluster_match_results.append(_async_handle_single_cluster_match( hass, zha_device, cluster, @@ -357,27 +360,28 @@ async def _handle_single_cluster_matches(hass, endpoint, zha_device, )) if cluster.cluster_id in EVENT_RELAY_CLUSTERS: - event_channel_tasks.append(_create_cluster_channel( + _async_create_cluster_channel( cluster, zha_device, is_new_join, channel_class=EventRelayChannel - )) - await asyncio.gather(*event_channel_tasks) - cluster_match_results = await asyncio.gather(*cluster_match_tasks) + ) + for cluster_match in cluster_match_results: if cluster_match is not None: cluster_matches.append(cluster_match) return cluster_matches -async def _handle_channel_only_cluster_match( +@callback +def _async_handle_channel_only_cluster_match( zha_device, cluster, is_new_join): """Handle a channel only cluster match.""" - await _create_cluster_channel(cluster, zha_device, is_new_join) + _async_create_cluster_channel(cluster, zha_device, is_new_join) -async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, +@callback +def _async_handle_single_cluster_match(hass, zha_device, cluster, device_key, device_classes, is_new_join): """Dispatch a single cluster match to a HA component.""" component = None # sub_component = None @@ -392,7 +396,7 @@ async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, if component is None or component not in COMPONENTS: return channels = [] - await _create_cluster_channel(cluster, zha_device, is_new_join, + _async_create_cluster_channel(cluster, zha_device, is_new_join, channels=channels) cluster_key = "{}-{}".format(device_key, cluster.cluster_id) @@ -416,7 +420,8 @@ async def _handle_single_cluster_match(hass, zha_device, cluster, device_key, return discovery_info -def _create_device_entity(zha_device): +@callback +def _async_create_device_entity(zha_device): """Create ZHADeviceEntity.""" device_entity_channels = [] if POWER_CONFIGURATION_CHANNEL in zha_device.cluster_channels: From c4400be62acb91e75a2580825eaebf9b8e1b9d89 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 26 Feb 2019 15:20:16 -0500 Subject: [PATCH 174/253] Add friendly name to devices in the device registry (#21318) * add friendly name to devices in the device registry * switch to name_by_user * review comments --- .../components/config/device_registry.py | 10 +++++++--- homeassistant/helpers/device_registry.py | 20 ++++++++++++++----- .../components/config/test_device_registry.py | 5 +++++ tests/helpers/test_device_registry.py | 9 +++++++-- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 0677531242a..9554f6aeee6 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -19,6 +19,7 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_UPDATE, vol.Required('device_id'): str, vol.Optional('area_id'): vol.Any(str, None), + vol.Optional('name_by_user'): vol.Any(str, None), }) @@ -49,11 +50,13 @@ async def websocket_update_device(hass, connection, msg): """Handle update area websocket command.""" registry = await async_get_registry(hass) - entry = registry.async_update_device( - msg['device_id'], area_id=msg['area_id']) + msg.pop('type') + msg_id = msg.pop('id') + + entry = registry.async_update_device(**msg) connection.send_message(websocket_api.result_message( - msg['id'], _entry_dict(entry) + msg_id, _entry_dict(entry) )) @@ -70,4 +73,5 @@ def _entry_dict(entry): 'id': entry.id, 'hub_device_id': entry.hub_device_id, 'area_id': entry.area_id, + 'name_by_user': entry.name_by_user, } diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 83827cca235..21c3b0d0209 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -37,6 +37,7 @@ class DeviceEntry: sw_version = attr.ib(type=str, default=None) hub_device_id = attr.ib(type=str, default=None) area_id = attr.ib(type=str, default=None) + name_by_user = attr.ib(type=str, default=None) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) @@ -124,9 +125,11 @@ class DeviceRegistry: ) @callback - def async_update_device(self, device_id, *, area_id=_UNDEF): + def async_update_device( + self, device_id, *, area_id=_UNDEF, name_by_user=_UNDEF): """Update properties of a device.""" - return self._async_update_device(device_id, area_id=area_id) + return self._async_update_device( + device_id, area_id=area_id, name_by_user=name_by_user) @callback def _async_update_device(self, device_id, *, add_config_entry_id=_UNDEF, @@ -138,7 +141,8 @@ class DeviceRegistry: name=_UNDEF, sw_version=_UNDEF, hub_device_id=_UNDEF, - area_id=_UNDEF): + area_id=_UNDEF, + name_by_user=_UNDEF): """Update device attributes.""" old = self.devices[device_id] @@ -179,6 +183,10 @@ class DeviceRegistry: if (area_id is not _UNDEF and area_id != old.area_id): changes['area_id'] = area_id + if (name_by_user is not _UNDEF and + name_by_user != old.name_by_user): + changes['name_by_user'] = name_by_user + if not changes: return old @@ -208,7 +216,8 @@ class DeviceRegistry: # Introduced in 0.79 hub_device_id=device.get('hub_device_id'), # Introduced in 0.87 - area_id=device.get('area_id') + area_id=device.get('area_id'), + name_by_user=device.get('name_by_user') ) self.devices = devices @@ -234,7 +243,8 @@ class DeviceRegistry: 'sw_version': entry.sw_version, 'id': entry.id, 'hub_device_id': entry.hub_device_id, - 'area_id': entry.area_id + 'area_id': entry.area_id, + 'name_by_user': entry.name_by_user } for entry in self.devices.values() ] diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index aa1b9e4e2d4..de603707ae2 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -49,6 +49,7 @@ async def test_list_devices(hass, client, registry): 'sw_version': None, 'hub_device_id': None, 'area_id': None, + 'name_by_user': None, }, { 'config_entries': ['1234'], @@ -59,6 +60,7 @@ async def test_list_devices(hass, client, registry): 'sw_version': None, 'hub_device_id': dev1, 'area_id': None, + 'name_by_user': None, } ] @@ -72,11 +74,13 @@ async def test_update_device(hass, client, registry): manufacturer='manufacturer', model='model') assert not device.area_id + assert not device.name_by_user await client.send_json({ 'id': 1, 'device_id': device.id, 'area_id': '12345A', + 'name_by_user': 'Test Friendly Name', 'type': 'config/device_registry/update', }) @@ -84,4 +88,5 @@ async def test_update_device(hass, client, registry): assert msg['result']['id'] == device.id assert msg['result']['area_id'] == '12345A' + assert msg['result']['name_by_user'] == 'Test Friendly Name' assert len(registry.devices) == 1 diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 93fffaa4ecc..caf1dafdf8f 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -133,7 +133,8 @@ async def test_loading_from_storage(hass, hass_storage): 'model': 'model', 'name': 'name', 'sw_version': 'version', - 'area_id': '12345A' + 'area_id': '12345A', + 'name_by_user': 'Test Friendly Name' } ] } @@ -148,6 +149,7 @@ async def test_loading_from_storage(hass, hass_storage): manufacturer='manufacturer', model='model') assert entry.id == 'abcdefghijklm' assert entry.area_id == '12345A' + assert entry.name_by_user == 'Test Friendly Name' assert isinstance(entry.config_entries, set) @@ -360,8 +362,11 @@ async def test_update(registry): }) assert not entry.area_id + assert not entry.name_by_user - updated_entry = registry.async_update_device(entry.id, area_id='12345A') + updated_entry = registry.async_update_device( + entry.id, area_id='12345A', name_by_user='Test Friendly Name') assert updated_entry != entry assert updated_entry.area_id == '12345A' + assert updated_entry.name_by_user == 'Test Friendly Name' From ab73b725e1c74f361a7785947d85ead6dc9bf297 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 26 Feb 2019 12:33:40 -0800 Subject: [PATCH 175/253] Pin isort (#21463) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 775425eb58b..06cf212204e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,3 +26,7 @@ pycrypto==1000000000.0.0 # Contains code to modify Home Assistant to work around our rules python-systemair-savecair==1000000000.0.0 + +# Newer version causes pylint to take forever +# https://github.com/timothycrosley/isort/issues/848 +isort==4.3.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 1355b71b7b1..a42e5471f8c 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -160,6 +160,10 @@ pycrypto==1000000000.0.0 # Contains code to modify Home Assistant to work around our rules python-systemair-savecair==1000000000.0.0 + +# Newer version causes pylint to take forever +# https://github.com/timothycrosley/isort/issues/848 +isort==4.3.4 """ From 29187795a8f03f4c1b5ad5f4943f760f98315b7e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 26 Feb 2019 21:35:25 +0100 Subject: [PATCH 176/253] Fix ESPHome nodes being auto-added without user confirmation (#21444) --- .../components/esphome/config_flow.py | 47 ++++++++++++------- homeassistant/components/esphome/strings.json | 6 ++- tests/components/esphome/test_config_flow.py | 5 ++ 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index e509455c12e..f6b8bb9abd7 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -26,18 +26,7 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): error: Optional[str] = None): """Handle a flow initialized by the user.""" if user_input is not None: - self._host = user_input['host'] - self._port = user_input['port'] - error, device_info = await self.fetch_device_info() - if error is not None: - return await self.async_step_user(error=error) - self._name = device_info.name - - # Only show authentication step if device uses password - if device_info.uses_password: - return await self.async_step_authenticate() - - return self._async_get_entry() + return await self._async_authenticate_or_add(user_input) fields = OrderedDict() fields[vol.Required('host', default=self._host or vol.UNDEFINED)] = str @@ -53,6 +42,33 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): errors=errors ) + async def _async_authenticate_or_add(self, user_input, + from_discovery=False): + self._host = user_input['host'] + self._port = user_input['port'] + error, device_info = await self.fetch_device_info() + if error is not None: + return await self.async_step_user(error=error) + self._name = device_info.name + # Only show authentication step if device uses password + if device_info.uses_password: + return await self.async_step_authenticate() + + if from_discovery: + # If from discovery, do not create entry immediately, + # First present user with message + return await self.async_step_discovery_confirm() + return self._async_get_entry() + + async def async_step_discovery_confirm(self, user_input=None): + """Handle user-confirmation of discovered node.""" + if user_input is not None: + return self._async_get_entry() + return self.async_show_form( + step_id='discovery_confirm', + description_placeholders={'name': self._name}, + ) + async def async_step_discovery(self, user_input: ConfigType): """Handle discovery.""" address = user_input['properties'].get( @@ -63,12 +79,10 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): reason='already_configured' ) - # Prefer .local addresses (mDNS is available after all, otherwise - # we wouldn't have received the discovery message) - return await self.async_step_user(user_input={ + return await self._async_authenticate_or_add(user_input={ 'host': address, 'port': user_input['port'], - }) + }, from_discovery=True) def _async_get_entry(self): return self.async_create_entry( @@ -99,6 +113,7 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): data_schema=vol.Schema({ vol.Required('password'): str }), + description_placeholders={'name': self._name}, errors=errors ) diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 56eeed8ea41..8f691d9cb00 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -21,8 +21,12 @@ "data": { "password": "Password" }, - "description": "Please enter the password you set in your configuration.", + "description": "Please enter the password you set in your configuration for {name}.", "title": "Enter Password" + }, + "discovery_confirm": { + "description": "Do you want to add the ESPHome node `{name}` to Home Assistant?", + "title": "Discovered ESPHome node" } }, "title": "ESPHome" diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 8c870c6ad73..076ec0066a6 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -203,6 +203,11 @@ async def test_discovery_initiation(hass, mock_client): MockDeviceInfo(False, "test8266")) result = await flow.async_step_discovery(user_input=service_info) + assert result['type'] == 'form' + assert result['step_id'] == 'discovery_confirm' + assert result['description_placeholders']['name'] == 'test8266' + + result = await flow.async_step_discovery_confirm(user_input={}) assert result['type'] == 'create_entry' assert result['title'] == 'test8266' assert result['data']['host'] == 'test8266.local' From dc6fd780a9e1d41c347e15ec1e79f5f1194edb1a Mon Sep 17 00:00:00 2001 From: "Patrick T.C" <124277+ptc@users.noreply.github.com> Date: Tue, 26 Feb 2019 21:37:01 +0100 Subject: [PATCH 177/253] Fix for Snips platform update that breaks hermes api. (#21443) --- homeassistant/components/snips/__init__.py | 6 ++--- tests/components/snips/test_init.py | 28 +++++++++++----------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 9a5508c8f32..44ce63c488a 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -105,10 +105,10 @@ async def async_setup(hass, config): _LOGGER.error('Received invalid JSON: %s', payload) return - if (request['intent']['probability'] + if (request['intent']['confidenceScore'] < config[DOMAIN].get(CONF_PROBABILITY)): _LOGGER.warning("Intent below probaility threshold %s < %s", - request['intent']['probability'], + request['intent']['confidenceScore'], config[DOMAIN].get(CONF_PROBABILITY)) return @@ -130,7 +130,7 @@ async def async_setup(hass, config): 'value': slot['rawValue']} slots['site_id'] = {'value': request.get('siteId')} slots['session_id'] = {'value': request.get('sessionId')} - slots['probability'] = {'value': request['intent']['probability']} + slots['confidenceScore'] = {'value': request['intent']['confidenceScore']} try: intent_response = await intent.async_handle( diff --git a/tests/components/snips/test_init.py b/tests/components/snips/test_init.py index 7bcfb11ff5c..e9719c02395 100644 --- a/tests/components/snips/test_init.py +++ b/tests/components/snips/test_init.py @@ -113,7 +113,7 @@ async def test_snips_intent(hass): "input": "turn the lights green", "intent": { "intentName": "Lights", - "probability": 1 + "confidenceScore": 1 }, "slots": [ { @@ -140,7 +140,7 @@ async def test_snips_intent(hass): assert intent assert intent.slots == {'light_color': {'value': 'green'}, 'light_color_raw': {'value': 'green'}, - 'probability': {'value': 1}, + 'confidenceScore': {'value': 1}, 'site_id': {'value': 'default'}, 'session_id': {'value': '1234567890ABCDEF'}} assert intent.text_input == 'turn the lights green' @@ -161,7 +161,7 @@ async def test_snips_service_intent(hass): "input": "turn the light on", "intent": { "intentName": "Lights", - "probability": 0.85 + "confidenceScore": 0.85 }, "siteId": "default", "slots": [ @@ -188,7 +188,7 @@ async def test_snips_service_intent(hass): assert calls[0].domain == 'light' assert calls[0].service == 'turn_on' assert calls[0].data['entity_id'] == 'light.kitchen' - assert 'probability' not in calls[0].data + assert 'confidenceScore' not in calls[0].data assert 'site_id' not in calls[0].data @@ -205,7 +205,7 @@ async def test_snips_intent_with_duration(hass): "input": "set a timer of five minutes", "intent": { "intentName": "SetTimer", - "probability": 1 + "confidenceScore": 1 }, "slots": [ { @@ -241,7 +241,7 @@ async def test_snips_intent_with_duration(hass): intent = intents[0] assert intent.platform == 'snips' assert intent.intent_type == 'SetTimer' - assert intent.slots == {'probability': {'value': 1}, + assert intent.slots == {'confidenceScore': {'value': 1}, 'site_id': {'value': None}, 'session_id': {'value': None}, 'timer_duration': {'value': 300}, @@ -274,7 +274,7 @@ async def test_intent_speech_response(hass): "sessionId": "abcdef0123456789", "intent": { "intentName": "spokenIntent", - "probability": 1 + "confidenceScore": 1 }, "slots": [] } @@ -306,7 +306,7 @@ async def test_unknown_intent(hass, caplog): "sessionId": "abcdef1234567890", "intent": { "intentName": "unknownIntent", - "probability": 1 + "confidenceScore": 1 }, "slots": [] } @@ -330,7 +330,7 @@ async def test_snips_intent_user(hass): "input": "what to do", "intent": { "intentName": "user_ABCDEF123__Lights", - "probability": 1 + "confidenceScore": 1 }, "slots": [] } @@ -359,7 +359,7 @@ async def test_snips_intent_username(hass): "input": "what to do", "intent": { "intentName": "username:Lights", - "probability": 1 + "confidenceScore": 1 }, "slots": [] } @@ -391,7 +391,7 @@ async def test_snips_low_probability(hass, caplog): "input": "I am not sure what to say", "intent": { "intentName": "LightsMaybe", - "probability": 0.49 + "confidenceScore": 0.49 }, "slots": [] } @@ -419,7 +419,7 @@ async def test_intent_special_slots(hass): "action": { "service": "light.turn_on", "data_template": { - "probability": "{{ probability }}", + "confidenceScore": "{{ confidenceScore }}", "site_id": "{{ site_id }}" } } @@ -432,7 +432,7 @@ async def test_intent_special_slots(hass): "input": "turn the light on", "intent": { "intentName": "Lights", - "probability": 0.85 + "confidenceScore": 0.85 }, "siteId": "default", "slots": [] @@ -444,7 +444,7 @@ async def test_intent_special_slots(hass): assert len(calls) == 1 assert calls[0].domain == 'light' assert calls[0].service == 'turn_on' - assert calls[0].data['probability'] == '0.85' + assert calls[0].data['confidenceScore'] == '0.85' assert calls[0].data['site_id'] == 'default' From e119deafe5f97e7f8b3744be6ae5ece2f287a79d Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 26 Feb 2019 21:37:21 +0100 Subject: [PATCH 178/253] Bump PyXiaomiGateway version to 0.11.2 (#21453) --- homeassistant/components/xiaomi_aqara/__init__.py | 2 +- homeassistant/components/xiaomi_aqara/light.py | 2 ++ requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index ce943fb2c93..5e47adc47f9 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['PyXiaomiGateway==0.11.1'] +REQUIREMENTS = ['PyXiaomiGateway==0.11.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_aqara/light.py b/homeassistant/components/xiaomi_aqara/light.py index 8ad0f2522d2..30433ccea3d 100644 --- a/homeassistant/components/xiaomi_aqara/light.py +++ b/homeassistant/components/xiaomi_aqara/light.py @@ -99,8 +99,10 @@ class XiaomiGatewayLight(XiaomiDevice, Light): if self._write_to_hub(self._sid, **{self._data_key: rgbhex}): self._state = True + self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the light off.""" if self._write_to_hub(self._sid, **{self._data_key: 0}): self._state = False + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index e4899715228..c4ca56e9ae9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -63,7 +63,7 @@ PySwitchbot==0.5 PyTransportNSW==0.1.1 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.11.1 +PyXiaomiGateway==0.11.2 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.5 From 344e839becaa8518f23b3c377e756c56a1f78456 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 26 Feb 2019 13:06:27 -0800 Subject: [PATCH 179/253] Remove launching a server in a test (#21445) --- tests/components/media_player/test_demo.py | 91 ++++++++-------------- 1 file changed, 31 insertions(+), 60 deletions(-) diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py index b213cf0b5c1..8cbe0a594d2 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -6,22 +6,13 @@ import asyncio import pytest import voluptuous as vol -from homeassistant.setup import setup_component -from homeassistant.const import HTTP_HEADER_HA_AUTH +from homeassistant.setup import setup_component, async_setup_component import homeassistant.components.media_player as mp -import homeassistant.components.http as http from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION -import requests - -from tests.common import get_test_home_assistant, get_test_instance_port +from tests.common import get_test_home_assistant from tests.components.media_player import common -SERVER_PORT = get_test_instance_port() -HTTP_BASE_URL = 'http://127.0.0.1:{}'.format(SERVER_PORT) -API_PASSWORD = "test1234" -HA_HEADERS = {HTTP_HEADER_HA_AUTH: API_PASSWORD} - entity_id = 'media_player.walkman' @@ -231,61 +222,41 @@ class TestDemoMediaPlayer(unittest.TestCase): assert mock_seek.called -class TestMediaPlayerWeb(unittest.TestCase): - """Test the media player web views sensor.""" +async def test_media_image_proxy(hass, hass_client): + """Test the media server image proxy server .""" + assert await async_setup_component( + hass, mp.DOMAIN, + {'media_player': {'platform': 'demo'}}) - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() + fake_picture_data = 'test.test' - assert setup_component(self.hass, http.DOMAIN, { - http.DOMAIN: { - http.CONF_SERVER_PORT: SERVER_PORT, - http.CONF_API_PASSWORD: API_PASSWORD, - }, - }) + class MockResponse(): + def __init__(self): + self.status = 200 + self.headers = {'Content-Type': 'sometype'} - assert setup_component( - self.hass, mp.DOMAIN, - {'media_player': {'platform': 'demo'}}) + @asyncio.coroutine + def read(self): + return fake_picture_data.encode('ascii') - self.hass.start() + @asyncio.coroutine + def release(self): + pass - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() + class MockWebsession(): - def test_media_image_proxy(self): - """Test the media server image proxy server .""" - fake_picture_data = 'test.test' + @asyncio.coroutine + def get(self, url): + return MockResponse() - class MockResponse(): - def __init__(self): - self.status = 200 - self.headers = {'Content-Type': 'sometype'} + def detach(self): + pass - @asyncio.coroutine - def read(self): - return fake_picture_data.encode('ascii') + hass.data[DATA_CLIENTSESSION] = MockWebsession() - @asyncio.coroutine - def release(self): - pass - - class MockWebsession(): - - @asyncio.coroutine - def get(self, url): - return MockResponse() - - def detach(self): - pass - - self.hass.data[DATA_CLIENTSESSION] = MockWebsession() - - assert self.hass.states.is_state(entity_id, 'playing') - state = self.hass.states.get(entity_id) - req = requests.get(HTTP_BASE_URL + - state.attributes.get('entity_picture')) - assert req.status_code == 200 - assert req.text == fake_picture_data + assert hass.states.is_state(entity_id, 'playing') + state = hass.states.get(entity_id) + client = await hass_client() + req = await client.get(state.attributes.get('entity_picture')) + assert req.status == 200 + assert await req.text() == fake_picture_data From 3b9db880656a8d74516e915fbbe3439f49d40a2c Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Tue, 26 Feb 2019 15:12:24 -0600 Subject: [PATCH 180/253] Add SmartThings Scene platform (#21405) * Add SmartThings Scene platform * Fixed failing tests after rebase * Update cover tests. --- .../components/smartthings/__init__.py | 26 ++++++++- homeassistant/components/smartthings/const.py | 3 +- homeassistant/components/smartthings/scene.py | 50 ++++++++++++++++ tests/components/smartthings/conftest.py | 33 ++++++++++- .../smartthings/test_binary_sensor.py | 9 +-- tests/components/smartthings/test_climate.py | 24 ++++---- tests/components/smartthings/test_cover.py | 16 +++--- tests/components/smartthings/test_fan.py | 16 +++--- tests/components/smartthings/test_init.py | 57 ++++++++++++++++--- tests/components/smartthings/test_light.py | 22 +++---- tests/components/smartthings/test_lock.py | 10 ++-- tests/components/smartthings/test_scene.py | 54 ++++++++++++++++++ tests/components/smartthings/test_sensor.py | 8 +-- tests/components/smartthings/test_switch.py | 10 ++-- 14 files changed, 266 insertions(+), 72 deletions(-) create mode 100644 homeassistant/components/smartthings/scene.py create mode 100644 tests/components/smartthings/test_scene.py diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 53ff6169c0a..e64988b2697 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .config_flow import SmartThingsFlowHandler # noqa from .const import ( - CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_OAUTH_CLIENT_ID, + CONF_APP_ID, CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, CONF_OAUTH_CLIENT_ID, CONF_OAUTH_CLIENT_SECRET, CONF_REFRESH_TOKEN, DATA_BROKERS, DATA_MANAGER, DOMAIN, EVENT_BUTTON, SIGNAL_SMARTTHINGS_UPDATE, SUPPORTED_PLATFORMS, TOKEN_REFRESH_INTERVAL) @@ -93,6 +93,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): installed_app = await validate_installed_app( api, entry.data[CONF_INSTALLED_APP_ID]) + # Get scenes + scenes = await async_get_entry_scenes(entry, api) + # Get SmartApp token to sync subscriptions token = await api.generate_tokens( entry.data[CONF_OAUTH_CLIENT_ID], @@ -123,7 +126,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): installed_app.installed_app_id, devices) # Setup device broker - broker = DeviceBroker(hass, entry, token, smart_app, devices) + broker = DeviceBroker(hass, entry, token, smart_app, devices, scenes) broker.connect() hass.data[DOMAIN][DATA_BROKERS][entry.entry_id] = broker @@ -156,6 +159,20 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): return True +async def async_get_entry_scenes(entry: ConfigEntry, api): + """Get the scenes within an integration.""" + try: + return await api.scenes(location_id=entry.data[CONF_LOCATION_ID]) + except ClientResponseError as ex: + if ex.status == 403: + _LOGGER.exception("Unable to load scenes for config entry '%s' " + "because the access token does not have the " + "required access", entry.title) + else: + raise + return [] + + async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS].pop(entry.entry_id, None) @@ -171,7 +188,7 @@ class DeviceBroker: """Manages an individual SmartThings config entry.""" def __init__(self, hass: HomeAssistantType, entry: ConfigEntry, - token, smart_app, devices: Iterable): + token, smart_app, devices: Iterable, scenes: Iterable): """Create a new instance of the DeviceBroker.""" self._hass = hass self._entry = entry @@ -182,6 +199,7 @@ class DeviceBroker: self._regenerate_token_remove = None self._assignments = self._assign_capabilities(devices) self.devices = {device.device_id: device for device in devices} + self.scenes = {scene.scene_id: scene for scene in scenes} def _assign_capabilities(self, devices: Iterable): """Assign platforms to capabilities.""" @@ -192,6 +210,8 @@ class DeviceBroker: for platform_name in SUPPORTED_PLATFORMS: platform = importlib.import_module( '.' + platform_name, self.__module__) + if not hasattr(platform, 'get_capabilities'): + continue assigned = platform.get_capabilities(capabilities) if not assigned: continue diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index 5da43203e4f..105c9760e12 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -34,7 +34,8 @@ SUPPORTED_PLATFORMS = [ 'cover', 'switch', 'binary_sensor', - 'sensor' + 'sensor', + 'scene' ] TOKEN_REFRESH_INTERVAL = timedelta(days=14) VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]" \ diff --git a/homeassistant/components/smartthings/scene.py b/homeassistant/components/smartthings/scene.py new file mode 100644 index 00000000000..9bf3211d8e3 --- /dev/null +++ b/homeassistant/components/smartthings/scene.py @@ -0,0 +1,50 @@ +"""Support for scenes through the SmartThings cloud API.""" +from homeassistant.components.scene import Scene + +from .const import DATA_BROKERS, DOMAIN + +DEPENDENCIES = ['smartthings'] + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add switches for a config entry.""" + broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] + async_add_entities( + [SmartThingsScene(scene) for scene in broker.scenes.values()]) + + +class SmartThingsScene(Scene): + """Define a SmartThings scene.""" + + def __init__(self, scene): + """Init the scene class.""" + self._scene = scene + + async def async_activate(self): + """Activate scene.""" + await self._scene.execute() + + @property + def device_state_attributes(self): + """Get attributes about the state.""" + return { + 'icon': self._scene.icon, + 'color': self._scene.color, + 'location_id': self._scene.location_id + } + + @property + def name(self) -> str: + """Return the name of the device.""" + return self._scene.name + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._scene.scene_id diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 4622e49b0c6..27e833bff25 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -5,7 +5,7 @@ from uuid import uuid4 from pysmartthings import ( CLASSIFICATION_AUTOMATION, AppEntity, AppOAuthClient, AppSettings, - DeviceEntity, InstalledApp, Location, Subscription) + DeviceEntity, InstalledApp, Location, SceneEntity, Subscription) from pysmartthings.api import Api import pytest @@ -24,13 +24,15 @@ from homeassistant.setup import async_setup_component from tests.common import mock_coro -async def setup_platform(hass, platform: str, *devices): +async def setup_platform(hass, platform: str, *, + devices=None, scenes=None): """Set up the SmartThings platform and prerequisites.""" hass.config.components.add(DOMAIN) config_entry = ConfigEntry(2, DOMAIN, "Test", {CONF_INSTALLED_APP_ID: str(uuid4())}, SOURCE_USER, CONN_CLASS_CLOUD_PUSH) - broker = DeviceBroker(hass, config_entry, Mock(), Mock(), devices) + broker = DeviceBroker(hass, config_entry, Mock(), Mock(), + devices or [], scenes or []) hass.data[DOMAIN] = { DATA_BROKERS: { @@ -295,6 +297,31 @@ def device_factory_fixture(): return _factory +@pytest.fixture(name="scene_factory") +def scene_factory_fixture(location): + """Fixture for creating mock devices.""" + api = Mock(spec=Api) + api.execute_scene.side_effect = \ + lambda *args, **kwargs: mock_coro(return_value={}) + + def _factory(name): + scene_data = { + 'sceneId': str(uuid4()), + 'sceneName': name, + 'sceneIcon': '', + 'sceneColor': '', + 'locationId': location.location_id + } + return SceneEntity(api, scene_data) + return _factory + + +@pytest.fixture(name="scene") +def scene_fixture(scene_factory): + """Fixture for an individual scene.""" + return scene_factory('Test Scene') + + @pytest.fixture(name="event_factory") def event_factory_fixture(): """Fixture for creating mock devices.""" diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 6e60ee49ca6..d1de9f8f020 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -40,7 +40,7 @@ async def test_entity_state(hass, device_factory): """Tests the state attributes properly match the light types.""" device = device_factory('Motion Sensor 1', [Capability.motion_sensor], {Attribute.motion: 'inactive'}) - await setup_platform(hass, BINARY_SENSOR_DOMAIN, device) + await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) state = hass.states.get('binary_sensor.motion_sensor_1_motion') assert state.state == 'off' assert state.attributes[ATTR_FRIENDLY_NAME] ==\ @@ -55,7 +55,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, BINARY_SENSOR_DOMAIN, device) + await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('binary_sensor.motion_sensor_1_motion') assert entry @@ -73,7 +73,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Motion Sensor 1', [Capability.motion_sensor], {Attribute.motion: 'inactive'}) - await setup_platform(hass, BINARY_SENSOR_DOMAIN, device) + await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) device.status.apply_attribute_update( 'main', Capability.motion_sensor, Attribute.motion, 'active') # Act @@ -91,7 +91,8 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Motion Sensor 1', [Capability.motion_sensor], {Attribute.motion: 'inactive'}) - config_entry = await setup_platform(hass, BINARY_SENSOR_DOMAIN, device) + config_entry = await setup_platform(hass, BINARY_SENSOR_DOMAIN, + devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'binary_sensor') diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 481e43266fa..29134d6ba6a 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -122,7 +122,7 @@ async def test_async_setup_platform(): async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): """Tests the state attributes properly match the thermostat type.""" - await setup_platform(hass, CLIMATE_DOMAIN, legacy_thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[legacy_thermostat]) state = hass.states.get('climate.legacy_thermostat') assert state.state == STATE_AUTO assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ @@ -141,7 +141,7 @@ async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): async def test_basic_thermostat_entity_state(hass, basic_thermostat): """Tests the state attributes properly match the thermostat type.""" - await setup_platform(hass, CLIMATE_DOMAIN, basic_thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[basic_thermostat]) state = hass.states.get('climate.basic_thermostat') assert state.state == STATE_OFF assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ @@ -155,7 +155,7 @@ async def test_basic_thermostat_entity_state(hass, basic_thermostat): async def test_thermostat_entity_state(hass, thermostat): """Tests the state attributes properly match the thermostat type.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) state = hass.states.get('climate.thermostat') assert state.state == STATE_HEAT assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ @@ -174,7 +174,7 @@ async def test_thermostat_entity_state(hass, thermostat): async def test_buggy_thermostat_entity_state(hass, buggy_thermostat): """Tests the state attributes properly match the thermostat type.""" - await setup_platform(hass, CLIMATE_DOMAIN, buggy_thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[buggy_thermostat]) state = hass.states.get('climate.buggy_thermostat') assert state.state == STATE_UNKNOWN assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ @@ -190,14 +190,14 @@ async def test_buggy_thermostat_invalid_mode(hass, buggy_thermostat): buggy_thermostat.status.update_attribute_value( Attribute.supported_thermostat_modes, ['heat', 'emergency heat', 'other']) - await setup_platform(hass, CLIMATE_DOMAIN, buggy_thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[buggy_thermostat]) state = hass.states.get('climate.buggy_thermostat') assert state.attributes[ATTR_OPERATION_LIST] == {'heat'} async def test_set_fan_mode(hass, thermostat): """Test the fan mode is set successfully.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_FAN_MODE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -209,7 +209,7 @@ async def test_set_fan_mode(hass, thermostat): async def test_set_operation_mode(hass, thermostat): """Test the operation mode is set successfully.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_OPERATION_MODE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -222,7 +222,7 @@ async def test_set_operation_mode(hass, thermostat): async def test_set_temperature_heat_mode(hass, thermostat): """Test the temperature is set successfully when in heat mode.""" thermostat.status.thermostat_mode = 'heat' - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -237,7 +237,7 @@ async def test_set_temperature_heat_mode(hass, thermostat): async def test_set_temperature_cool_mode(hass, thermostat): """Test the temperature is set successfully when in cool mode.""" thermostat.status.thermostat_mode = 'cool' - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -250,7 +250,7 @@ async def test_set_temperature_cool_mode(hass, thermostat): async def test_set_temperature(hass, thermostat): """Test the temperature is set successfully.""" thermostat.status.thermostat_mode = 'auto' - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -264,7 +264,7 @@ async def test_set_temperature(hass, thermostat): async def test_set_temperature_with_mode(hass, thermostat): """Test the temperature and mode is set successfully.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { ATTR_ENTITY_ID: 'climate.thermostat', @@ -280,7 +280,7 @@ async def test_set_temperature_with_mode(hass, thermostat): async def test_entity_and_device_attributes(hass, thermostat): """Test the attributes of the entries are correct.""" - await setup_platform(hass, CLIMATE_DOMAIN, thermostat) + await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 6e7844e521c..7e41237e3e7 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -32,7 +32,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, COVER_DOMAIN, device) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('cover.garage') assert entry @@ -57,7 +57,7 @@ async def test_open(hass, device_factory): device_factory('Shade', [Capability.window_shade], {Attribute.window_shade: 'closed'}) } - await setup_platform(hass, COVER_DOMAIN, *devices) + await setup_platform(hass, COVER_DOMAIN, devices=devices) entity_ids = [ 'cover.door', 'cover.garage', @@ -86,7 +86,7 @@ async def test_close(hass, device_factory): device_factory('Shade', [Capability.window_shade], {Attribute.window_shade: 'open'}) } - await setup_platform(hass, COVER_DOMAIN, *devices) + await setup_platform(hass, COVER_DOMAIN, devices=devices) entity_ids = [ 'cover.door', 'cover.garage', @@ -113,7 +113,7 @@ async def test_set_cover_position(hass, device_factory): Capability.switch_level], {Attribute.window_shade: 'opening', Attribute.battery: 95, Attribute.level: 10}) - await setup_platform(hass, COVER_DOMAIN, device) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Act await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, @@ -136,7 +136,7 @@ async def test_set_cover_position_unsupported(hass, device_factory): 'Shade', [Capability.window_shade], {Attribute.window_shade: 'opening'}) - await setup_platform(hass, COVER_DOMAIN, device) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Act await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, @@ -152,7 +152,7 @@ async def test_update_to_open_from_signal(hass, device_factory): # Arrange device = device_factory('Garage', [Capability.garage_door_control], {Attribute.door: 'opening'}) - await setup_platform(hass, COVER_DOMAIN, device) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) device.status.update_attribute_value(Attribute.door, 'open') assert hass.states.get('cover.garage').state == STATE_OPENING # Act @@ -170,7 +170,7 @@ async def test_update_to_closed_from_signal(hass, device_factory): # Arrange device = device_factory('Garage', [Capability.garage_door_control], {Attribute.door: 'closing'}) - await setup_platform(hass, COVER_DOMAIN, device) + await setup_platform(hass, COVER_DOMAIN, devices=[device]) device.status.update_attribute_value(Attribute.door, 'closed') assert hass.states.get('cover.garage').state == STATE_CLOSING # Act @@ -188,7 +188,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Garage', [Capability.garage_door_control], {Attribute.door: 'open'}) - config_entry = await setup_platform(hass, COVER_DOMAIN, device) + config_entry = await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, COVER_DOMAIN) diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 644c0823fd5..dffffa7b340 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -29,7 +29,7 @@ async def test_entity_state(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'on', Attribute.fan_speed: 2}) - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Dimmer 1 state = hass.states.get('fan.fan_1') @@ -48,7 +48,7 @@ async def test_entity_and_device_attributes(hass, device_factory): capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'on', Attribute.fan_speed: 2}) # Act - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Assert @@ -71,7 +71,7 @@ async def test_turn_off(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'on', Attribute.fan_speed: 2}) - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'fan', 'turn_off', {'entity_id': 'fan.fan_1'}, @@ -89,7 +89,7 @@ async def test_turn_on(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'fan', 'turn_on', {ATTR_ENTITY_ID: "fan.fan_1"}, @@ -107,7 +107,7 @@ async def test_turn_on_with_speed(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'fan', 'turn_on', @@ -128,7 +128,7 @@ async def test_set_speed(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'fan', 'set_speed', @@ -149,7 +149,7 @@ async def test_update_from_signal(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - await setup_platform(hass, FAN_DOMAIN, device) + await setup_platform(hass, FAN_DOMAIN, devices=[device]) await device.switch_on(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -168,7 +168,7 @@ async def test_unload_config_entry(hass, device_factory): "Fan 1", capabilities=[Capability.switch, Capability.fan_speed], status={Attribute.switch: 'off', Attribute.fan_speed: 0}) - config_entry = await setup_platform(hass, FAN_DOMAIN, device) + config_entry = await setup_platform(hass, FAN_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'fan') diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 0e35ef80fc2..ec0b3982517 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -77,6 +77,19 @@ async def test_recoverable_api_errors_raise_not_ready( await smartthings.async_setup_entry(hass, config_entry) +async def test_scenes_api_errors_raise_not_ready( + hass, config_entry, app, installed_app, smartthings_mock): + """Test if scenes are unauthorized we continue to load platforms.""" + setattr(hass.config_entries, '_entries', [config_entry]) + api = smartthings_mock.return_value + api.app.return_value = mock_coro(return_value=app) + api.installed_app.return_value = mock_coro(return_value=installed_app) + api.scenes.return_value = mock_coro( + exception=ClientResponseError(None, None, status=500)) + with pytest.raises(ConfigEntryNotReady): + await smartthings.async_setup_entry(hass, config_entry) + + async def test_connection_errors_raise_not_ready( hass, config_entry, smartthings_mock): """Test config entry not ready raised for connection errors.""" @@ -118,17 +131,45 @@ async def test_unauthorized_installed_app_raises_not_ready( await smartthings.async_setup_entry(hass, config_entry) -async def test_config_entry_loads_platforms( +async def test_scenes_unauthorized_loads_platforms( hass, config_entry, app, installed_app, device, smartthings_mock, subscription_factory): - """Test config entry loads properly and proxies to platforms.""" + """Test if scenes are unauthorized we continue to load platforms.""" setattr(hass.config_entries, '_entries', [config_entry]) - api = smartthings_mock.return_value api.app.return_value = mock_coro(return_value=app) api.installed_app.return_value = mock_coro(return_value=installed_app) api.devices.side_effect = \ lambda *args, **kwargs: mock_coro(return_value=[device]) + api.scenes.return_value = mock_coro( + exception=ClientResponseError(None, None, status=403)) + mock_token = Mock() + mock_token.access_token.return_value = str(uuid4()) + mock_token.refresh_token.return_value = str(uuid4()) + api.generate_tokens.return_value = mock_coro(return_value=mock_token) + subscriptions = [subscription_factory(capability) + for capability in device.capabilities] + api.subscriptions.return_value = mock_coro(return_value=subscriptions) + + with patch.object(hass.config_entries, 'async_forward_entry_setup', + return_value=mock_coro()) as forward_mock: + assert await smartthings.async_setup_entry(hass, config_entry) + # Assert platforms loaded + await hass.async_block_till_done() + assert forward_mock.call_count == len(SUPPORTED_PLATFORMS) + + +async def test_config_entry_loads_platforms( + hass, config_entry, app, installed_app, + device, smartthings_mock, subscription_factory, scene): + """Test config entry loads properly and proxies to platforms.""" + setattr(hass.config_entries, '_entries', [config_entry]) + api = smartthings_mock.return_value + api.app.return_value = mock_coro(return_value=app) + api.installed_app.return_value = mock_coro(return_value=installed_app) + api.devices.side_effect = \ + lambda *args, **kwargs: mock_coro(return_value=[device]) + api.scenes.return_value = mock_coro(return_value=[scene]) mock_token = Mock() mock_token.access_token.return_value = str(uuid4()) mock_token.refresh_token.return_value = str(uuid4()) @@ -151,7 +192,7 @@ async def test_unload_entry(hass, config_entry): smart_app = Mock() smart_app.connect_event.return_value = connect_disconnect broker = smartthings.DeviceBroker( - hass, config_entry, Mock(), smart_app, []) + hass, config_entry, Mock(), smart_app, [], []) broker.connect() hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] = broker @@ -184,7 +225,7 @@ async def test_broker_regenerates_token( '.async_track_time_interval', new=async_track_time_interval): broker = smartthings.DeviceBroker( - hass, config_entry, token, Mock(), []) + hass, config_entry, token, Mock(), [], []) broker.connect() assert stored_action @@ -214,7 +255,7 @@ async def test_event_handler_dispatches_updated_devices( async_dispatcher_connect(hass, SIGNAL_SMARTTHINGS_UPDATE, signal) broker = smartthings.DeviceBroker( - hass, config_entry, Mock(), Mock(), devices) + hass, config_entry, Mock(), Mock(), devices, []) broker.connect() # pylint:disable=protected-access @@ -238,7 +279,7 @@ async def test_event_handler_ignores_other_installed_app( called = True async_dispatcher_connect(hass, SIGNAL_SMARTTHINGS_UPDATE, signal) broker = smartthings.DeviceBroker( - hass, config_entry, Mock(), Mock(), [device]) + hass, config_entry, Mock(), Mock(), [device], []) broker.connect() # pylint:disable=protected-access @@ -271,7 +312,7 @@ async def test_event_handler_fires_button_events( } hass.bus.async_listen(EVENT_BUTTON, handler) broker = smartthings.DeviceBroker( - hass, config_entry, Mock(), Mock(), [device]) + hass, config_entry, Mock(), Mock(), [device], []) broker.connect() # pylint:disable=protected-access diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index d31507925d6..6efd88d7237 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -52,7 +52,7 @@ async def test_async_setup_platform(): async def test_entity_state(hass, light_devices): """Tests the state attributes properly match the light types.""" - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Dimmer 1 state = hass.states.get('light.dimmer_1') @@ -86,7 +86,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, LIGHT_DOMAIN, device) + await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get("light.light_1") assert entry @@ -103,7 +103,7 @@ async def test_entity_and_device_attributes(hass, device_factory): async def test_turn_off(hass, light_devices): """Test the light turns of successfully.""" # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_off', {'entity_id': 'light.color_dimmer_2'}, @@ -117,7 +117,7 @@ async def test_turn_off(hass, light_devices): async def test_turn_off_with_transition(hass, light_devices): """Test the light turns of successfully with transition.""" # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_off', @@ -132,7 +132,7 @@ async def test_turn_off_with_transition(hass, light_devices): async def test_turn_on(hass, light_devices): """Test the light turns of successfully.""" # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', {ATTR_ENTITY_ID: "light.color_dimmer_1"}, @@ -146,7 +146,7 @@ async def test_turn_on(hass, light_devices): async def test_turn_on_with_brightness(hass, light_devices): """Test the light turns on to the specified brightness.""" # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -170,7 +170,7 @@ async def test_turn_on_with_minimal_brightness(hass, light_devices): set the level to zero, which turns off the lights in SmartThings. """ # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -188,7 +188,7 @@ async def test_turn_on_with_minimal_brightness(hass, light_devices): async def test_turn_on_with_color(hass, light_devices): """Test the light turns on with color.""" # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -205,7 +205,7 @@ async def test_turn_on_with_color(hass, light_devices): async def test_turn_on_with_color_temp(hass, light_devices): """Test the light turns on with color temp.""" # Arrange - await setup_platform(hass, LIGHT_DOMAIN, *light_devices) + await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) # Act await hass.services.async_call( 'light', 'turn_on', @@ -229,7 +229,7 @@ async def test_update_from_signal(hass, device_factory): status={Attribute.switch: 'off', Attribute.level: 100, Attribute.hue: 76.0, Attribute.saturation: 55.0, Attribute.color_temperature: 4500}) - await setup_platform(hass, LIGHT_DOMAIN, device) + await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) await device.switch_on(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -251,7 +251,7 @@ async def test_unload_config_entry(hass, device_factory): status={Attribute.switch: 'off', Attribute.level: 100, Attribute.hue: 76.0, Attribute.saturation: 55.0, Attribute.color_temperature: 4500}) - config_entry = await setup_platform(hass, LIGHT_DOMAIN, device) + config_entry = await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'light') diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 922abbb161f..1d98e5f9bdb 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -29,7 +29,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, LOCK_DOMAIN, device) + await setup_platform(hass, LOCK_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('lock.lock_1') assert entry @@ -55,7 +55,7 @@ async def test_lock(hass, device_factory): 'lockName': 'Front Door', 'usedCode': 'Code 2' }) - await setup_platform(hass, LOCK_DOMAIN, device) + await setup_platform(hass, LOCK_DOMAIN, devices=[device]) # Act await hass.services.async_call( LOCK_DOMAIN, 'lock', {'entity_id': 'lock.lock_1'}, @@ -77,7 +77,7 @@ async def test_unlock(hass, device_factory): # Arrange device = device_factory('Lock_1', [Capability.lock], {Attribute.lock: 'locked'}) - await setup_platform(hass, LOCK_DOMAIN, device) + await setup_platform(hass, LOCK_DOMAIN, devices=[device]) # Act await hass.services.async_call( LOCK_DOMAIN, 'unlock', {'entity_id': 'lock.lock_1'}, @@ -93,7 +93,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Lock_1', [Capability.lock], {Attribute.lock: 'unlocked'}) - await setup_platform(hass, LOCK_DOMAIN, device) + await setup_platform(hass, LOCK_DOMAIN, devices=[device]) await device.lock(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -110,7 +110,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Lock_1', [Capability.lock], {Attribute.lock: 'locked'}) - config_entry = await setup_platform(hass, LOCK_DOMAIN, device) + config_entry = await setup_platform(hass, LOCK_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'lock') diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py new file mode 100644 index 00000000000..2d4990675f8 --- /dev/null +++ b/tests/components/smartthings/test_scene.py @@ -0,0 +1,54 @@ +""" +Test for the SmartThings scene platform. + +The only mocking required is of the underlying SmartThings API object so +real HTTP calls are not initiated during testing. +""" +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN +from homeassistant.components.smartthings import scene as scene_platform +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON + +from .conftest import setup_platform + + +async def test_async_setup_platform(): + """Test setup platform does nothing (it uses config entries).""" + await scene_platform.async_setup_platform(None, None, None) + + +async def test_entity_and_device_attributes(hass, scene): + """Test the attributes of the entity are correct.""" + # Arrange + entity_registry = await hass.helpers.entity_registry.async_get_registry() + # Act + await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) + # Assert + entry = entity_registry.async_get('scene.test_scene') + assert entry + assert entry.unique_id == scene.scene_id + + +async def test_scene_activate(hass, scene): + """Test the scene is activated.""" + await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) + await hass.services.async_call( + SCENE_DOMAIN, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: 'scene.test_scene'}, + blocking=True) + state = hass.states.get('scene.test_scene') + assert state.attributes['icon'] == scene.icon + assert state.attributes['color'] == scene.color + assert state.attributes['location_id'] == scene.location_id + # pylint: disable=protected-access + assert scene._api.execute_scene.call_count == 1 # type: ignore + + +async def test_unload_config_entry(hass, scene): + """Test the scene is removed when the config entry is unloaded.""" + # Arrange + config_entry = await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) + # Act + await hass.config_entries.async_forward_entry_unload( + config_entry, SCENE_DOMAIN) + # Assert + assert not hass.states.get('scene.test_scene') diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 773f157dd87..879aae1994d 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -37,7 +37,7 @@ async def test_entity_state(hass, device_factory): """Tests the state attributes properly match the light types.""" device = device_factory('Sensor 1', [Capability.battery], {Attribute.battery: 100}) - await setup_platform(hass, SENSOR_DOMAIN, device) + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) state = hass.states.get('sensor.sensor_1_battery') assert state.state == '100' assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == '%' @@ -53,7 +53,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, SENSOR_DOMAIN, device) + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('sensor.sensor_1_battery') assert entry @@ -71,7 +71,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Sensor 1', [Capability.battery], {Attribute.battery: 100}) - await setup_platform(hass, SENSOR_DOMAIN, device) + await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) device.status.apply_attribute_update( 'main', Capability.battery, Attribute.battery, 75) # Act @@ -89,7 +89,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Sensor 1', [Capability.battery], {Attribute.battery: 100}) - config_entry = await setup_platform(hass, SENSOR_DOMAIN, device) + config_entry = await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'sensor') diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 7d21db00460..e3b1f46bf39 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -29,7 +29,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entity_registry = await hass.helpers.entity_registry.async_get_registry() device_registry = await hass.helpers.device_registry.async_get_registry() # Act - await setup_platform(hass, SWITCH_DOMAIN, device) + await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) # Assert entry = entity_registry.async_get('switch.switch_1') assert entry @@ -48,7 +48,7 @@ async def test_turn_off(hass, device_factory): # Arrange device = device_factory('Switch_1', [Capability.switch], {Attribute.switch: 'on'}) - await setup_platform(hass, SWITCH_DOMAIN, device) + await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'switch', 'turn_off', {'entity_id': 'switch.switch_1'}, @@ -69,7 +69,7 @@ async def test_turn_on(hass, device_factory): {Attribute.switch: 'off', Attribute.power: 355, Attribute.energy: 11.422}) - await setup_platform(hass, SWITCH_DOMAIN, device) + await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) # Act await hass.services.async_call( 'switch', 'turn_on', {'entity_id': 'switch.switch_1'}, @@ -87,7 +87,7 @@ async def test_update_from_signal(hass, device_factory): # Arrange device = device_factory('Switch_1', [Capability.switch], {Attribute.switch: 'off'}) - await setup_platform(hass, SWITCH_DOMAIN, device) + await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) await device.switch_on(True) # Act async_dispatcher_send(hass, SIGNAL_SMARTTHINGS_UPDATE, @@ -104,7 +104,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory('Switch 1', [Capability.switch], {Attribute.switch: 'on'}) - config_entry = await setup_platform(hass, SWITCH_DOMAIN, device) + config_entry = await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) # Act await hass.config_entries.async_forward_entry_unload( config_entry, 'switch') From aa472d4f1005e91e9371ecf9242ce0c1df41ce12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9-Marc=20Simard?= Date: Tue, 26 Feb 2019 16:17:20 -0500 Subject: [PATCH 181/253] Adjust GTFS dates when crossing midnight (#20916) --- homeassistant/components/sensor/gtfs.py | 30 ++++++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/gtfs.py b/homeassistant/components/sensor/gtfs.py index 94f21287e39..eec08be093f 100644 --- a/homeassistant/components/sensor/gtfs.py +++ b/homeassistant/components/sensor/gtfs.py @@ -40,6 +40,7 @@ ICONS = { 7: 'mdi:stairs', } +DATE_FORMAT = '%Y-%m-%d' TIME_FORMAT = '%Y-%m-%d %H:%M:%S' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -59,7 +60,7 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): now = datetime.datetime.now() + offset day_name = now.strftime('%A').lower() now_str = now.strftime('%H:%M:%S') - today = now.strftime('%Y-%m-%d') + today = now.strftime(DATE_FORMAT) from sqlalchemy.sql import text @@ -69,7 +70,7 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): time(origin_stop_time.departure_time) AS origin_depart_time, origin_stop_time.drop_off_type AS origin_drop_off_type, origin_stop_time.pickup_type AS origin_pickup_type, - origin_stop_time.shape_dist_traveled AS origin_shape_dist_traveled, + origin_stop_time.shape_dist_traveled AS origin_dist_traveled, origin_stop_time.stop_headsign AS origin_stop_headsign, origin_stop_time.stop_sequence AS origin_stop_sequence, time(destination_stop_time.arrival_time) AS dest_arrival_time, @@ -111,10 +112,27 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): if item == {}: return None - origin_arrival_time = '{} {}'.format(today, item['origin_arrival_time']) + # Format arrival and departure dates and times, accounting for the + # possibility of times crossing over midnight. + origin_arrival = now + if item['origin_arrival_time'] > item['origin_depart_time']: + origin_arrival -= datetime.timedelta(days=1) + origin_arrival_time = '{} {}'.format(origin_arrival.strftime(DATE_FORMAT), + item['origin_arrival_time']) + origin_depart_time = '{} {}'.format(today, item['origin_depart_time']) - dest_arrival_time = '{} {}'.format(today, item['dest_arrival_time']) - dest_depart_time = '{} {}'.format(today, item['dest_depart_time']) + + dest_arrival = now + if item['dest_arrival_time'] < item['origin_depart_time']: + dest_arrival += datetime.timedelta(days=1) + dest_arrival_time = '{} {}'.format(dest_arrival.strftime(DATE_FORMAT), + item['dest_arrival_time']) + + dest_depart = dest_arrival + if item['dest_depart_time'] < item['dest_arrival_time']: + dest_depart += datetime.timedelta(days=1) + dest_depart_time = '{} {}'.format(dest_depart.strftime(DATE_FORMAT), + item['dest_depart_time']) depart_time = datetime.datetime.strptime(origin_depart_time, TIME_FORMAT) arrival_time = datetime.datetime.strptime(dest_arrival_time, TIME_FORMAT) @@ -129,7 +147,7 @@ def get_next_departure(sched, start_station_id, end_station_id, offset): 'Departure Time': origin_depart_time, 'Drop Off Type': item['origin_drop_off_type'], 'Pickup Type': item['origin_pickup_type'], - 'Shape Dist Traveled': item['origin_shape_dist_traveled'], + 'Shape Dist Traveled': item['origin_dist_traveled'], 'Headsign': item['origin_stop_headsign'], 'Sequence': item['origin_stop_sequence'] } From e739fd8a310bc4a5cc9dd25f016c603c78c230fa Mon Sep 17 00:00:00 2001 From: Leonardo Merza Date: Tue, 26 Feb 2019 16:55:11 -0500 Subject: [PATCH 182/253] Reddit Sensor (#21344) * init automated commit 22/02/2019 22:55:49 cr comments cr comments automated commit 24/02/2019 14:41:08 automated commit 24/02/2019 14:41:59 automated commit 24/02/2019 14:54:16 automated commit 24/02/2019 14:54:49 automated commit 24/02/2019 19:46:15 automated commit 25/02/2019 10:10:46 automated commit 25/02/2019 10:10:52 automated commit 25/02/2019 10:12:16 automated commit 25/02/2019 10:15:59 * automated commit 25/02/2019 12:26:38 * automated commit 25/02/2019 13:55:52 --- .coveragerc | 1 + homeassistant/components/reddit/__init__.py | 1 + homeassistant/components/reddit/sensor.py | 125 ++++++++++++++++++++ requirements_all.txt | 3 + 4 files changed, 130 insertions(+) create mode 100644 homeassistant/components/reddit/__init__.py create mode 100644 homeassistant/components/reddit/sensor.py diff --git a/.coveragerc b/.coveragerc index 2616a1163bc..f8829939682 100644 --- a/.coveragerc +++ b/.coveragerc @@ -395,6 +395,7 @@ omit = homeassistant/components/rainmachine/switch.py homeassistant/components/raspihats/* homeassistant/components/raspyrfm/* + homeassistant/components/reddit/* homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote/harmony.py homeassistant/components/remote/itach.py diff --git a/homeassistant/components/reddit/__init__.py b/homeassistant/components/reddit/__init__.py new file mode 100644 index 00000000000..3c810cdb1d8 --- /dev/null +++ b/homeassistant/components/reddit/__init__.py @@ -0,0 +1 @@ +"""Reddit Component.""" diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py new file mode 100644 index 00000000000..1b6a960669c --- /dev/null +++ b/homeassistant/components/reddit/sensor.py @@ -0,0 +1,125 @@ +"""Support for Reddit.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, CONF_MAXIMUM) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['praw==6.1.1'] + +_LOGGER = logging.getLogger(__name__) + +CONF_CLIENT_ID = 'client_id' +CONF_CLIENT_SECRET = 'client_secret' +CONF_SUBREDDITS = 'subreddits' + +ATTR_ID = 'id' +ATTR_BODY = 'body' +ATTR_COMMENTS_NUMBER = 'comms_num' +ATTR_CREATED = 'created' +ATTR_POSTS = 'posts' +ATTR_SUBREDDIT = 'subreddit' +ATTR_SCORE = 'score' +ATTR_TITLE = 'title' +ATTR_URL = 'url' + +DEFAULT_NAME = 'Reddit' + +SCAN_INTERVAL = timedelta(seconds=300) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_SUBREDDITS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_MAXIMUM, default=10): cv.positive_int +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Reddit sensor platform.""" + import praw + + subreddits = config[CONF_SUBREDDITS] + user_agent = '{}_home_assistant_sensor'.format(config[CONF_USERNAME]) + limit = config[CONF_MAXIMUM] + + try: + reddit = praw.Reddit( + client_id=config[CONF_CLIENT_ID], + client_secret=config[CONF_CLIENT_SECRET], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + user_agent=user_agent) + + _LOGGER.debug('Connected to praw') + + except praw.exceptions.PRAWException as err: + _LOGGER.error("Reddit error %s", err) + return + + sensors = [RedditSensor(reddit, sub, limit) for sub in subreddits] + add_entities(sensors, True) + + +class RedditSensor(Entity): + """Representation of a Reddit sensor.""" + + def __init__(self, reddit, subreddit: str, limit: int): + """Initialize the Reddit sensor.""" + self._reddit = reddit + self._limit = limit + self._subreddit = subreddit + + self._subreddit_data = [] + + @property + def name(self): + """Return the name of the sensor.""" + return 'reddit_{}'.format(self._subreddit) + + @property + def state(self): + """Return the state of the sensor.""" + return len(self._subreddit_data) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_SUBREDDIT: self._subreddit, + ATTR_POSTS: self._subreddit_data + } + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return 'mdi:reddit' + + def update(self): + """Update data from Reddit API.""" + import praw + + self._subreddit_data = [] + + try: + subreddit = self._reddit.subreddit(self._subreddit) + + for submission in subreddit.top(limit=self._limit): + self._subreddit_data.append({ + ATTR_ID: submission.id, + ATTR_URL: submission.url, + ATTR_TITLE: submission.title, + ATTR_SCORE: submission.score, + ATTR_COMMENTS_NUMBER: submission.num_comments, + ATTR_CREATED: submission.created, + ATTR_BODY: submission.selftext + }) + + except praw.exceptions.PRAWException as err: + _LOGGER.error("Reddit error %s", err) diff --git a/requirements_all.txt b/requirements_all.txt index c4ca56e9ae9..6b289c6af88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -840,6 +840,9 @@ pocketcasts==0.1 # homeassistant.components.sensor.postnl postnl_api==1.0.2 +# homeassistant.components.reddit.sensor +praw==6.1.1 + # homeassistant.components.sensor.islamic_prayer_times prayer_times_calculator==0.0.3 From 5c2f997394653ac51133a6848df5774d2be20d3d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 26 Feb 2019 14:42:09 -0800 Subject: [PATCH 183/253] Lint --- homeassistant/components/snips/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 44ce63c488a..20cc7137ef8 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -130,7 +130,9 @@ async def async_setup(hass, config): 'value': slot['rawValue']} slots['site_id'] = {'value': request.get('siteId')} slots['session_id'] = {'value': request.get('sessionId')} - slots['confidenceScore'] = {'value': request['intent']['confidenceScore']} + slots['confidenceScore'] = { + 'value': request['intent']['confidenceScore'] + } try: intent_response = await intent.async_handle( From 7bae76843cba4657ee39c150b8634dfa87fc5d15 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Tue, 26 Feb 2019 14:42:48 -0800 Subject: [PATCH 184/253] Add config for trusted networks auth provider (#21111) * Add config for trusted networks auth provider * Lint * Fix typing * Fix pylint * Fix lint * Add some log information * Add http.trusted_networks deprecated warning * Remove log info * Lint --- .../auth/providers/trusted_networks.py | 34 ++++++++------ homeassistant/bootstrap.py | 7 ++- homeassistant/components/http/__init__.py | 13 +++++- homeassistant/config.py | 9 ++-- tests/auth/providers/test_trusted_networks.py | 45 ++++++++++--------- tests/test_config.py | 7 ++- 6 files changed, 72 insertions(+), 43 deletions(-) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 8a7e1d67c6d..d0bc45c326a 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -3,18 +3,23 @@ It shows list of users if access from trusted network. Abort login flow if not access from trusted network. """ -from typing import Any, Dict, Optional, cast +from ipaddress import ip_network, IPv4Address, IPv6Address, IPv4Network,\ + IPv6Network +from typing import Any, Dict, List, Optional, Union, cast import voluptuous as vol -from homeassistant.components.http import HomeAssistantHTTP # noqa: F401 +import homeassistant.helpers.config_validation as cv from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError - from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow from ..models import Credentials, UserMeta +IPAddress = Union[IPv4Address, IPv6Address] +IPNetwork = Union[IPv4Network, IPv6Network] + CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({ + vol.Required('trusted_networks'): vol.All(cv.ensure_list, [ip_network]) }, extra=vol.PREVENT_EXTRA) @@ -35,6 +40,11 @@ class TrustedNetworksAuthProvider(AuthProvider): DEFAULT_TITLE = 'Trusted Networks' + @property + def trusted_networks(self) -> List[IPNetwork]: + """Return trusted networks.""" + return cast(List[IPNetwork], self.config['trusted_networks']) + @property def support_mfa(self) -> bool: """Trusted Networks auth provider does not support MFA.""" @@ -49,7 +59,7 @@ class TrustedNetworksAuthProvider(AuthProvider): if not user.system_generated and user.is_active} return TrustedNetworksLoginFlow( - self, cast(str, context.get('ip_address')), available_users) + self, cast(IPAddress, context.get('ip_address')), available_users) async def async_get_or_create_credentials( self, flow_result: Dict[str, str]) -> Credentials: @@ -80,19 +90,17 @@ class TrustedNetworksAuthProvider(AuthProvider): raise NotImplementedError @callback - def async_validate_access(self, ip_address: str) -> None: + def async_validate_access(self, ip_addr: IPAddress) -> None: """Make sure the access from trusted networks. Raise InvalidAuthError if not. Raise InvalidAuthError if trusted_networks is not configured. """ - hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP - - if not hass_http or not hass_http.trusted_networks: + if not self.trusted_networks: raise InvalidAuthError('trusted_networks is not configured') - if not any(ip_address in trusted_network for trusted_network - in hass_http.trusted_networks): + if not any(ip_addr in trusted_network for trusted_network + in self.trusted_networks): raise InvalidAuthError('Not in trusted_networks') @@ -100,12 +108,12 @@ class TrustedNetworksLoginFlow(LoginFlow): """Handler for the login flow.""" def __init__(self, auth_provider: TrustedNetworksAuthProvider, - ip_address: str, available_users: Dict[str, Optional[str]]) \ - -> None: + ip_addr: IPAddress, + available_users: Dict[str, Optional[str]]) -> None: """Initialize the login flow.""" super().__init__(auth_provider) self._available_users = available_users - self._ip_address = ip_address + self._ip_address = ip_addr async def async_step_init( self, user_input: Optional[Dict[str, str]] = None) \ diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a018d540033..ca01610bcf9 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -86,13 +86,12 @@ async def async_from_config_dict(config: Dict[str, Any], log_no_color) core_config = config.get(core.DOMAIN, {}) - has_api_password = bool((config.get('http') or {}).get('api_password')) - has_trusted_networks = bool((config.get('http') or {}) - .get('trusted_networks')) + has_api_password = bool(config.get('http', {}).get('api_password')) + trusted_networks = config.get('http', {}).get('trusted_networks') try: await conf_util.async_process_ha_core_config( - hass, core_config, has_api_password, has_trusted_networks) + hass, core_config, has_api_password, trusted_networks) except vol.Invalid as config_err: conf_util.async_log_exception( config_err, 'homeassistant', core_config, hass) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 7dca332058c..f57068081a5 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -52,6 +52,17 @@ DEFAULT_SERVER_HOST = '0.0.0.0' DEFAULT_DEVELOPMENT = '0' NO_LOGIN_ATTEMPT_THRESHOLD = -1 + +def trusted_networks_deprecated(value): + """Warn user trusted_networks config is deprecated.""" + _LOGGER.warning( + "Configuring trusted_networks via the http component has been" + " deprecated. Use the trusted networks auth provider instead." + " For instructions, see https://www.home-assistant.io/docs/" + "authentication/providers/#trusted-networks") + return value + + HTTP_SCHEMA = vol.Schema({ vol.Optional(CONF_API_PASSWORD): cv.string, vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string, @@ -66,7 +77,7 @@ HTTP_SCHEMA = vol.Schema({ vol.Inclusive(CONF_TRUSTED_PROXIES, 'proxy'): vol.All(cv.ensure_list, [ip_network]), vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): - vol.All(cv.ensure_list, [ip_network]), + vol.All(cv.ensure_list, [ip_network], trusted_networks_deprecated), vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD), diff --git a/homeassistant/config.py b/homeassistant/config.py index 3310cd3e160..492db240eee 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -429,7 +429,7 @@ def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: async def async_process_ha_core_config( hass: HomeAssistant, config: Dict, has_api_password: bool = False, - has_trusted_networks: bool = False) -> None: + trusted_networks: Optional[Any] = None) -> None: """Process the [homeassistant] section from the configuration. This method is a coroutine. @@ -446,8 +446,11 @@ async def async_process_ha_core_config( ] if has_api_password: auth_conf.append({'type': 'legacy_api_password'}) - if has_trusted_networks: - auth_conf.append({'type': 'trusted_networks'}) + if trusted_networks: + auth_conf.append({ + 'type': 'trusted_networks', + 'trusted_networks': trusted_networks, + }) mfa_conf = config.get(CONF_AUTH_MFA_MODULES, [ {'type': 'totp', 'id': 'totp', 'name': 'Authenticator app'}, diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 0ca302f8273..57e74e750d5 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -1,5 +1,5 @@ """Test the Trusted Networks auth provider.""" -from unittest.mock import Mock +from ipaddress import ip_address import pytest import voluptuous as vol @@ -18,9 +18,17 @@ def store(hass): @pytest.fixture def provider(hass, store): """Mock provider.""" - return tn_auth.TrustedNetworksAuthProvider(hass, store, { - 'type': 'trusted_networks' - }) + return tn_auth.TrustedNetworksAuthProvider( + hass, store, tn_auth.CONFIG_SCHEMA({ + 'type': 'trusted_networks', + 'trusted_networks': [ + '192.168.0.1', + '192.168.128.0/24', + '::1', + 'fd00::/8' + ] + }) + ) @pytest.fixture @@ -56,14 +64,17 @@ async def test_trusted_networks_credentials(manager, provider): async def test_validate_access(provider): """Test validate access from trusted networks.""" - with pytest.raises(tn_auth.InvalidAuthError): - provider.async_validate_access('192.168.0.1') - - provider.hass.http = Mock(trusted_networks=['192.168.0.1']) - provider.async_validate_access('192.168.0.1') + provider.async_validate_access(ip_address('192.168.0.1')) + provider.async_validate_access(ip_address('192.168.128.10')) + provider.async_validate_access(ip_address('::1')) + provider.async_validate_access(ip_address('fd01:db8::ff00:42:8329')) with pytest.raises(tn_auth.InvalidAuthError): - provider.async_validate_access('127.0.0.1') + provider.async_validate_access(ip_address('192.168.0.2')) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address('127.0.0.1')) + with pytest.raises(tn_auth.InvalidAuthError): + provider.async_validate_access(ip_address('2001:db8::ff00:42:8329')) async def test_login_flow(manager, provider): @@ -71,22 +82,16 @@ async def test_login_flow(manager, provider): owner = await manager.async_create_user("test-owner") user = await manager.async_create_user("test-user") - # trusted network didn't loaded - flow = await provider.async_login_flow({'ip_address': '127.0.0.1'}) - step = await flow.async_step_init() - assert step['type'] == 'abort' - assert step['reason'] == 'not_whitelisted' - - provider.hass.http = Mock(trusted_networks=['192.168.0.1']) - # not from trusted network - flow = await provider.async_login_flow({'ip_address': '127.0.0.1'}) + flow = await provider.async_login_flow( + {'ip_address': ip_address('127.0.0.1')}) step = await flow.async_step_init() assert step['type'] == 'abort' assert step['reason'] == 'not_whitelisted' # from trusted network, list users - flow = await provider.async_login_flow({'ip_address': '192.168.0.1'}) + flow = await provider.async_login_flow( + {'ip_address': ip_address('192.168.0.1')}) step = await flow.async_step_init() assert step['step_id'] == 'init' diff --git a/tests/test_config.py b/tests/test_config.py index 212fc247eb9..e860ff53b3d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,6 +5,7 @@ import os import unittest import unittest.mock as mock from collections import OrderedDict +from ipaddress import ip_network import asynctest import pytest @@ -891,12 +892,14 @@ async def test_auth_provider_config_default_trusted_networks(hass): } if hasattr(hass, 'auth'): del hass.auth - await config_util.async_process_ha_core_config(hass, core_config, - has_trusted_networks=True) + await config_util.async_process_ha_core_config( + hass, core_config, trusted_networks=['192.168.0.1']) assert len(hass.auth.auth_providers) == 2 assert hass.auth.auth_providers[0].type == 'homeassistant' assert hass.auth.auth_providers[1].type == 'trusted_networks' + assert hass.auth.auth_providers[1].trusted_networks[0] == ip_network( + '192.168.0.1') async def test_disallowed_auth_provider_config(hass): From 42e691c1945e18183cd872127a6294aefed4e3fb Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Tue, 26 Feb 2019 23:48:19 +0100 Subject: [PATCH 185/253] Add HomematicIP HmIP-OC8 module (#21401) * Add support for HmIP-OC8 module * Fix line lenght --- .../components/homematicip_cloud/switch.py | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 057673d8f9b..f129febb5e7 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -22,11 +22,12 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the HomematicIP switch from a config entry.""" - from homematicip.device import ( - PlugableSwitch, - PlugableSwitchMeasuring, - BrandSwitchMeasuring, - FullFlushSwitchMeasuring, + from homematicip.aio.device import ( + AsyncPlugableSwitch, + AsyncPlugableSwitchMeasuring, + AsyncBrandSwitchMeasuring, + AsyncFullFlushSwitchMeasuring, + AsyncOpenCollector8Module, ) from homematicip.group import SwitchingGroup @@ -34,16 +35,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: - if isinstance(device, BrandSwitchMeasuring): + if isinstance(device, AsyncBrandSwitchMeasuring): # BrandSwitchMeasuring inherits PlugableSwitchMeasuring # This device is implemented in the light platform and will # not be added in the switch platform pass - elif isinstance(device, (PlugableSwitchMeasuring, - FullFlushSwitchMeasuring)): + elif isinstance(device, (AsyncPlugableSwitchMeasuring, + AsyncFullFlushSwitchMeasuring)): devices.append(HomematicipSwitchMeasuring(home, device)) - elif isinstance(device, PlugableSwitch): + elif isinstance(device, AsyncPlugableSwitch): devices.append(HomematicipSwitch(home, device)) + elif isinstance(device, AsyncOpenCollector8Module): + for channel in range(1, 9): + devices.append(HomematicipMultiSwitch(home, device, channel)) for group in home.groups: if isinstance(group, SwitchingGroup): @@ -111,3 +115,31 @@ class HomematicipSwitchMeasuring(HomematicipSwitch): if self._device.energyCounter is None: return 0 return round(self._device.energyCounter) + + +class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): + """Representation of a HomematicIP Cloud multi switch device.""" + + def __init__(self, home, device, channel): + """Initialize the multi switch device.""" + self.channel = channel + super().__init__(home, device, 'Channel{}'.format(channel)) + + @property + def unique_id(self): + """Return a unique ID.""" + return "{}_{}_{}".format(self.__class__.__name__, + self.post, self._device.id) + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.functionalChannels[self.channel].on + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self._device.turn_on(self.channel) + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._device.turn_off(self.channel) From 9cff1dd4ba0be4d9b8a5f9642825f2f6c2785fff Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 26 Feb 2019 17:49:15 -0500 Subject: [PATCH 186/253] Added new econet states (#21420) --- .../components/water_heater/econet.py | 47 +++++++++++-------- requirements_all.txt | 2 +- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/water_heater/econet.py b/homeassistant/components/water_heater/econet.py index 93ae98ed94b..fc15ffbe821 100644 --- a/homeassistant/components/water_heater/econet.py +++ b/homeassistant/components/water_heater/econet.py @@ -13,7 +13,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyeconet==0.0.6'] +REQUIREMENTS = ['pyeconet==0.0.8'] _LOGGER = logging.getLogger(__name__) @@ -43,18 +43,18 @@ DELETE_VACATION_SCHEMA = vol.Schema({ ECONET_DATA = 'econet' -HA_STATE_TO_ECONET = { - STATE_ECO: 'Energy Saver', - STATE_ELECTRIC: 'Electric', - STATE_HEAT_PUMP: 'Heat Pump', - STATE_GAS: 'gas', - STATE_HIGH_DEMAND: 'High Demand', - STATE_OFF: 'Off', - STATE_PERFORMANCE: 'Performance' +ECONET_STATE_TO_HA = { + 'Energy Saver': STATE_ECO, + 'gas': STATE_GAS, + 'High Demand': STATE_HIGH_DEMAND, + 'Off': STATE_OFF, + 'Performance': STATE_PERFORMANCE, + 'Heat Pump Only': STATE_HEAT_PUMP, + 'Electric-Only': STATE_ELECTRIC, + 'Electric': STATE_ELECTRIC, + 'Heat Pump': STATE_HEAT_PUMP } -ECONET_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_ECONET.items()} - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, @@ -110,6 +110,18 @@ class EcoNetWaterHeater(WaterHeaterDevice): def __init__(self, water_heater): """Initialize the water heater.""" self.water_heater = water_heater + self.supported_modes = self.water_heater.supported_modes + self.econet_state_to_ha = {} + for mode in ECONET_STATE_TO_HA.keys(): + if mode in self.supported_modes: + self.econet_state_to_ha[mode] = ECONET_STATE_TO_HA.get(mode) + for key, value in self.econet_state_to_ha.itmes(): + self.ha_state_to_econet[value] = key + for mode in self.supported_modes: + if mode not in ECONET_STATE_TO_HA: + error = "Invalid operation mode mapping. " + mode + \ + " doesn't map. Please report this." + _LOGGER.error(error) @property def name(self): @@ -149,22 +161,17 @@ class EcoNetWaterHeater(WaterHeaterDevice): ["eco", "heat_pump", "high_demand", "electric_only"] """ - current_op = ECONET_STATE_TO_HA.get(self.water_heater.mode) + current_op = self.econet_state_to_ha.get(self.water_heater.mode) return current_op @property def operation_list(self): """List of available operation modes.""" op_list = [] - modes = self.water_heater.supported_modes - for mode in modes: - ha_mode = ECONET_STATE_TO_HA.get(mode) + for mode in self.supported_modes: + ha_mode = self.econet_state_to_ha.get(mode) if ha_mode is not None: op_list.append(ha_mode) - else: - error = "Invalid operation mode mapping. " + mode + \ - " doesn't map. Please report this." - _LOGGER.error(error) return op_list @property @@ -182,7 +189,7 @@ class EcoNetWaterHeater(WaterHeaterDevice): def set_operation_mode(self, operation_mode): """Set operation mode.""" - op_mode_to_set = HA_STATE_TO_ECONET.get(operation_mode) + op_mode_to_set = self.ha_state_to_econet.get(operation_mode) if op_mode_to_set is not None: self.water_heater.set_mode(op_mode_to_set) else: diff --git a/requirements_all.txt b/requirements_all.txt index 6b289c6af88..f67d46b61bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1003,7 +1003,7 @@ pydukeenergy==0.0.6 pyebox==1.1.4 # homeassistant.components.water_heater.econet -pyeconet==0.0.6 +pyeconet==0.0.8 # homeassistant.components.switch.edimax pyedimax==0.1 From efa48848a5c27a175a29c282fc40f203bbce6609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 26 Feb 2019 23:50:48 +0100 Subject: [PATCH 187/253] Comment out bluepy libraries from requirements_all.txt (#20856) * Comment bluepy libraries from requirements_all.txt, fixes #20778 * Comment bluepy libraries from requirements_all.txt, fixes #20778 * Comment bluepy libraries from requirements_all.txt, fixes #20778 --- homeassistant/components/switch/switchbot.py | 1 + homeassistant/components/switch/switchmate.py | 1 + requirements_all.txt | 4 +-- script/gen_requirements_all.py | 36 ++++++++++--------- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/switch/switchbot.py b/homeassistant/components/switch/switchbot.py index 9cd2927d832..a85357b525a 100644 --- a/homeassistant/components/switch/switchbot.py +++ b/homeassistant/components/switch/switchbot.py @@ -36,6 +36,7 @@ class SwitchBot(SwitchDevice): def __init__(self, mac, name) -> None: """Initialize the Switchbot.""" + # pylint: disable=import-error, no-member import switchbot self._state = False self._name = name diff --git a/homeassistant/components/switch/switchmate.py b/homeassistant/components/switch/switchmate.py index be80ef19169..5289ca6ec1b 100644 --- a/homeassistant/components/switch/switchmate.py +++ b/homeassistant/components/switch/switchmate.py @@ -42,6 +42,7 @@ class Switchmate(SwitchDevice): def __init__(self, mac, name, flip_on_off) -> None: """Initialize the Switchmate.""" + # pylint: disable=import-error, no-member, no-value-for-parameter import switchmate self._mac = mac self._name = name diff --git a/requirements_all.txt b/requirements_all.txt index f67d46b61bc..286b42ba214 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -57,7 +57,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.1.3 # homeassistant.components.switch.switchbot -PySwitchbot==0.5 +# PySwitchbot==0.5 # homeassistant.components.sensor.transport_nsw PyTransportNSW==0.1.1 @@ -906,7 +906,7 @@ pyMetno==0.4.6 pyRFXtrx==0.23 # homeassistant.components.switch.switchmate -pySwitchmate==0.4.5 +# pySwitchmate==0.4.5 # homeassistant.components.tibber pyTibber==0.9.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a42e5471f8c..09eb9f21d4a 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -8,31 +8,33 @@ import sys import fnmatch COMMENT_REQUIREMENTS = ( - 'RPi.GPIO', - 'raspihats', - 'rpi-rf', 'Adafruit-DHT', 'Adafruit_BBIO', - 'fritzconnection', - 'pybluez', + 'avion', 'beacontools', + 'blinkt', 'bluepy', + 'bme680', + 'credstash', + 'decora', + 'envirophat', + 'evdev', + 'face_recognition', + 'fritzconnection', + 'i2csense', 'opencv-python', + 'py_noaa', + 'pybluez', + 'pycups', + 'PySwitchbot', + 'pySwitchmate', + 'python-eq3bt', 'python-lirc', 'pyuserinput', - 'evdev', - 'pycups', - 'python-eq3bt', - 'avion', - 'decora', - 'face_recognition', - 'blinkt', + 'raspihats', + 'rpi-rf', + 'RPi.GPIO', 'smbus-cffi', - 'envirophat', - 'i2csense', - 'credstash', - 'bme680', - 'py_noaa', ) TEST_REQUIREMENTS = ( From 4d4cd2d752d4ab1a886924c8524eb2f41d89f6b4 Mon Sep 17 00:00:00 2001 From: Thibault Maekelbergh Date: Wed, 27 Feb 2019 00:08:25 +0100 Subject: [PATCH 188/253] Update sensor.nmbs to support vias better + show on map (#20063) * Add the destination stations to the attributes * Add support for showing station on map * Add option to exclude via connections * Cleanup the live sensor * Perform better checking against false i/o None values * Add support for excluding vias * Add more details for via trains * Lint file * Update logger level --- homeassistant/components/sensor/nmbs.py | 77 ++++++++++++++++++++----- 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sensor/nmbs.py b/homeassistant/components/sensor/nmbs.py index e13ca18af5f..e677a072ef3 100644 --- a/homeassistant/components/sensor/nmbs.py +++ b/homeassistant/components/sensor/nmbs.py @@ -9,7 +9,9 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, + CONF_SHOW_ON_MAP) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util @@ -17,7 +19,6 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'NMBS' -DEFAULT_NAME_LIVE = "NMBS Live" DEFAULT_ICON = "mdi:train" DEFAULT_ICON_ALERT = "mdi:alert-octagon" @@ -25,6 +26,7 @@ DEFAULT_ICON_ALERT = "mdi:alert-octagon" CONF_STATION_FROM = 'station_from' CONF_STATION_TO = 'station_to' CONF_STATION_LIVE = 'station_live' +CONF_EXCLUDE_VIAS = 'exclude_vias' REQUIREMENTS = ["pyrail==0.0.3"] @@ -32,7 +34,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_STATION_FROM): cv.string, vol.Required(CONF_STATION_TO): cv.string, vol.Optional(CONF_STATION_LIVE): cv.string, + vol.Optional(CONF_EXCLUDE_VIAS, default=False): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean, }) @@ -64,14 +68,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): api_client = iRail() name = config[CONF_NAME] + show_on_map = config[CONF_SHOW_ON_MAP] station_from = config[CONF_STATION_FROM] station_to = config[CONF_STATION_TO] station_live = config.get(CONF_STATION_LIVE) + excl_vias = config[CONF_EXCLUDE_VIAS] - sensors = [NMBSSensor(name, station_from, station_to, api_client)] + sensors = [NMBSSensor( + api_client, name, show_on_map, station_from, station_to, excl_vias)] if station_live is not None: - sensors.append(NMBSLiveBoard(station_live, api_client)) + sensors.append(NMBSLiveBoard(api_client, station_live)) add_entities(sensors, True) @@ -79,22 +86,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class NMBSLiveBoard(Entity): """Get the next train from a station's liveboard.""" - def __init__(self, live_station, api_client): + def __init__(self, api_client, live_station): """Initialize the sensor for getting liveboard data.""" self._station = live_station self._api_client = api_client + self._attrs = {} self._state = None @property def name(self): """Return the sensor default name.""" - return DEFAULT_NAME_LIVE + return "NMBS Live" @property def icon(self): """Return the default icon or an alert icon if delays.""" - if self._attrs is not None and int(self._attrs['delay']) > 0: + if self._attrs and int(self._attrs['delay']) > 0: return DEFAULT_ICON_ALERT return DEFAULT_ICON @@ -107,7 +115,7 @@ class NMBSLiveBoard(Entity): @property def device_state_attributes(self): """Return the sensor attributes if data is available.""" - if self._state is None or self._attrs is None: + if self._state is None or not self._attrs: return None delay = get_delay_in_minutes(self._attrs["delay"]) @@ -118,6 +126,7 @@ class NMBSLiveBoard(Entity): 'extra_train': int(self._attrs['isExtra']) > 0, 'occupancy': self._attrs['occupancy']['name'], 'vehicle_id': self._attrs['vehicle'], + 'monitored_station': self._station, ATTR_ATTRIBUTION: "https://api.irail.be/", } @@ -139,12 +148,16 @@ class NMBSLiveBoard(Entity): class NMBSSensor(Entity): """Get the the total travel time for a given connection.""" - def __init__(self, name, station_from, station_to, api_client): + def __init__(self, api_client, name, show_on_map, + station_from, station_to, excl_vias): """Initialize the NMBS connection sensor.""" self._name = name + self._show_on_map = show_on_map + self._api_client = api_client self._station_from = station_from self._station_to = station_to - self._api_client = api_client + self._excl_vias = excl_vias + self._attrs = {} self._state = None @@ -161,7 +174,7 @@ class NMBSSensor(Entity): @property def icon(self): """Return the sensor default icon or an alert icon if any delay.""" - if self._attrs is not None: + if self._attrs: delay = get_delay_in_minutes(self._attrs['departure']['delay']) if delay > 0: return "mdi:alert-octagon" @@ -171,7 +184,7 @@ class NMBSSensor(Entity): @property def device_state_attributes(self): """Return sensor attributes if data is available.""" - if self._state is None or self._attrs is None: + if self._state is None or not self._attrs: return None delay = get_delay_in_minutes(self._attrs['departure']['delay']) @@ -179,6 +192,7 @@ class NMBSSensor(Entity): attrs = { 'departure': "In {} minutes".format(departure), + 'destination': self._station_to, 'direction': self._attrs['departure']['direction']['name'], 'occupancy': self._attrs['departure']['occupancy']['name'], "platform_arriving": self._attrs['arrival']['platform'], @@ -187,6 +201,20 @@ class NMBSSensor(Entity): ATTR_ATTRIBUTION: "https://api.irail.be/", } + if self._show_on_map and self.station_coordinates: + attrs[ATTR_LATITUDE] = self.station_coordinates[0] + attrs[ATTR_LONGITUDE] = self.station_coordinates[1] + + if self.is_via_connection and not self._excl_vias: + via = self._attrs['vias']['via'][0] + + attrs['via'] = via['station'] + attrs['via_arrival_platform'] = via['arrival']['platform'] + attrs['via_transfer_platform'] = via['departure']['platform'] + attrs['via_transfer_time'] = get_delay_in_minutes( + via['timeBetween'] + ) + get_delay_in_minutes(via['departure']['delay']) + if delay > 0: attrs['delay'] = "{} minutes".format(delay) @@ -197,13 +225,29 @@ class NMBSSensor(Entity): """Return the state of the device.""" return self._state + @property + def station_coordinates(self): + """Get the lat, long coordinates for station.""" + if self._state is None or not self._attrs: + return [] + + latitude = float(self._attrs['departure']['stationinfo']['locationY']) + longitude = float(self._attrs['departure']['stationinfo']['locationX']) + return [latitude, longitude] + + @property + def is_via_connection(self): + """Return whether the connection goes through another station.""" + if not self._attrs: + return False + + return 'vias' in self._attrs and int(self._attrs['vias']['number']) > 0 + def update(self): """Set the state to the duration of a connection.""" connections = self._api_client.get_connections( self._station_from, self._station_to) - next_connection = None - if int(connections['connection'][0]['departure']['left']) > 0: next_connection = connections['connection'][1] else: @@ -211,6 +255,11 @@ class NMBSSensor(Entity): self._attrs = next_connection + if self._excl_vias and self.is_via_connection: + _LOGGER.debug("Skipping update of NMBSSensor \ + because this connection is a via") + return + duration = get_ride_duration( next_connection['departure']['time'], next_connection['arrival']['time'], From be78265631b1a2ad55833e0cfac06969256f3113 Mon Sep 17 00:00:00 2001 From: blackray12 Date: Wed, 27 Feb 2019 07:10:25 +0800 Subject: [PATCH 189/253] Add mitemp bt sensor device class (#20293) * Fix HomeKit missing humidity issue When using HomeKit components, MiTemp BT's humidity state will not display in Home.app. After Added home-assistant device class into property, this problem is solved. * Add Device_Class_Battery to property * Break long lines. * Lint --- homeassistant/components/sensor/mitemp_bt.py | 25 +++++++++++++------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/mitemp_bt.py b/homeassistant/components/sensor/mitemp_bt.py index 15e225fd2c0..f8bee17978d 100644 --- a/homeassistant/components/sensor/mitemp_bt.py +++ b/homeassistant/components/sensor/mitemp_bt.py @@ -12,7 +12,8 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC + CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_BATTERY ) @@ -37,9 +38,9 @@ DEFAULT_TIMEOUT = 10 # Sensor types are defined like: Name, units SENSOR_TYPES = { - 'temperature': ['Temperature', '°C'], - 'humidity': ['Humidity', '%'], - 'battery': ['Battery', '%'], + 'temperature': [DEVICE_CLASS_TEMPERATURE, 'Temperature', '°C'], + 'humidity': [DEVICE_CLASS_HUMIDITY, 'Humidity', '%'], + 'battery': [DEVICE_CLASS_BATTERY, 'Battery', '%'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -80,15 +81,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devs = [] for parameter in config[CONF_MONITORED_CONDITIONS]: - name = SENSOR_TYPES[parameter][0] - unit = SENSOR_TYPES[parameter][1] + device = SENSOR_TYPES[parameter][0] + name = SENSOR_TYPES[parameter][1] + unit = SENSOR_TYPES[parameter][2] prefix = config.get(CONF_NAME) if prefix: name = "{} {}".format(prefix, name) devs.append(MiTempBtSensor( - poller, parameter, name, unit, force_update, median)) + poller, parameter, device, name, unit, force_update, median)) add_entities(devs) @@ -96,10 +98,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class MiTempBtSensor(Entity): """Implementing the MiTempBt sensor.""" - def __init__(self, poller, parameter, name, unit, force_update, median): + def __init__(self, poller, parameter, device, name, unit, + force_update, median): """Initialize the sensor.""" self.poller = poller self.parameter = parameter + self._device = device self._unit = unit self._name = name self._state = None @@ -125,6 +129,11 @@ class MiTempBtSensor(Entity): """Return the units of measurement.""" return self._unit + @property + def device_class(self): + """Device class of this entity.""" + return self._device + @property def force_update(self): """Force update.""" From e9f79c3d06ed0ac6311712f527c2411605573663 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 26 Feb 2019 18:50:34 -0800 Subject: [PATCH 190/253] Lint --- homeassistant/components/water_heater/econet.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/water_heater/econet.py b/homeassistant/components/water_heater/econet.py index fc15ffbe821..69fde44bdd2 100644 --- a/homeassistant/components/water_heater/econet.py +++ b/homeassistant/components/water_heater/econet.py @@ -112,10 +112,11 @@ class EcoNetWaterHeater(WaterHeaterDevice): self.water_heater = water_heater self.supported_modes = self.water_heater.supported_modes self.econet_state_to_ha = {} - for mode in ECONET_STATE_TO_HA.keys(): + self.ha_state_to_econet = {} + for mode in ECONET_STATE_TO_HA: if mode in self.supported_modes: self.econet_state_to_ha[mode] = ECONET_STATE_TO_HA.get(mode) - for key, value in self.econet_state_to_ha.itmes(): + for key, value in self.econet_state_to_ha.items(): self.ha_state_to_econet[value] = key for mode in self.supported_modes: if mode not in ECONET_STATE_TO_HA: From 822b6328e1dcf21889fa190e7c1cb74951dd83cf Mon Sep 17 00:00:00 2001 From: dfournie <45941998+dfournie@users.noreply.github.com> Date: Wed, 27 Feb 2019 04:22:29 +0100 Subject: [PATCH 191/253] Add Somfy IO Garage door (#21320) * Add Somfy IO Garage door * Fix code style --- homeassistant/components/tahoma/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index e76cadc7ce3..1807667da87 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -43,6 +43,7 @@ TAHOMA_TYPES = { 'io:SomfyContactIOSystemSensor': 'sensor', 'io:VerticalExteriorAwningIOComponent': 'cover', 'io:WindowOpenerVeluxIOComponent': 'cover', + 'io:GarageOpenerIOComponent': 'cover', 'rtds:RTDSContactSensor': 'sensor', 'rtds:RTDSMotionSensor': 'sensor', 'rtds:RTDSSmokeSensor': 'smoke', From 51773f338efddc3837536c1b9f60e3e52a7712be Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Tue, 26 Feb 2019 23:02:49 -0500 Subject: [PATCH 192/253] Add person support for Waze Travel Time (#21471) Adds person to the list of TRACKABLE_DOMAINS domains. _get_location_from_entity will handle the person domain without any changes. --- homeassistant/components/sensor/waze_travel_time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index 0f8897de291..83b4f3ad934 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -44,7 +44,7 @@ REGIONS = ['US', 'NA', 'EU', 'IL', 'AU'] SCAN_INTERVAL = timedelta(minutes=5) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) -TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone'] +TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone', 'person'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ORIGIN): cv.string, From 28f9c7c2cd6f56dc31cfd636db56af235e4b09f3 Mon Sep 17 00:00:00 2001 From: Kevin Tuhumury Date: Wed, 27 Feb 2019 06:24:09 +0100 Subject: [PATCH 193/253] Add cpu_use_percent as a new resource to the Glances sensor. (#21455) --- homeassistant/components/sensor/glances.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 5ac0816a0c1..53db254e4b3 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -47,6 +47,7 @@ SENSOR_TYPES = { 'process_total': ['Total', 'Count', 'mdi:memory'], 'process_thread': ['Thread', 'Count', 'mdi:memory'], 'process_sleeping': ['Sleeping', 'Count', 'mdi:memory'], + 'cpu_use_percent': ['CPU used', '%', 'mdi:memory'], 'cpu_temp': ['CPU Temp', TEMP_CELSIUS, 'mdi:thermometer'], 'docker_active': ['Containers active', '', 'mdi:docker'], 'docker_cpu_use': ['Containers CPU used', '%', 'mdi:docker'], @@ -177,6 +178,8 @@ class GlancesSensor(Entity): self._state = value['processcount']['thread'] elif self.type == 'process_sleeping': self._state = value['processcount']['sleeping'] + elif self.type == 'cpu_use_percent': + self._state = value['quicklook']['cpu'] elif self.type == 'cpu_temp': for sensor in value['sensors']: if sensor['label'] in ['CPU', "Package id 0", From 33c9afd6e056c8269fbb60ed8bf324b07da9b388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomas=20Hellstr=C3=B6m?= Date: Wed, 27 Feb 2019 06:37:35 +0100 Subject: [PATCH 194/253] Added a digit for precipitation (#21439) --- homeassistant/components/smhi/__init__.py | 2 +- homeassistant/components/smhi/weather.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index 6af8c14843b..608ee9b6a6d 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import Config, HomeAssistant from .config_flow import smhi_locations # noqa: F401 from .const import DOMAIN # noqa: F401 -REQUIREMENTS = ['smhi-pkg==1.0.8'] +REQUIREMENTS = ['smhi-pkg==1.0.10'] DEFAULT_NAME = 'smhi' diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 75a0c51d010..6136d093a33 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -218,7 +218,7 @@ class SmhiWeather(WeatherEntity): ATTR_FORECAST_TEMP: forecast.temperature_max, ATTR_FORECAST_TEMP_LOW: forecast.temperature_min, ATTR_FORECAST_PRECIPITATION: - round(forecast.total_precipitation), + round(forecast.total_precipitation, 1), ATTR_FORECAST_CONDITION: condition, }) diff --git a/requirements_all.txt b/requirements_all.txt index 286b42ba214..6d327a817f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1588,7 +1588,7 @@ smappy==0.2.16 # smbus-cffi==0.5.1 # homeassistant.components.smhi -smhi-pkg==1.0.8 +smhi-pkg==1.0.10 # homeassistant.components.media_player.snapcast snapcast==2.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 307c283e8e9..493888baeb9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -275,7 +275,7 @@ simplisafe-python==3.1.14 sleepyq==0.6 # homeassistant.components.smhi -smhi-pkg==1.0.8 +smhi-pkg==1.0.10 # homeassistant.components.climate.honeywell somecomfort==0.5.2 From 01ee92177f9687ea8cebccf3d0e140b7cb1f83ec Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 27 Feb 2019 18:37:02 +0530 Subject: [PATCH 195/253] Upgrade bcrypt to 3.1.6 (#21476) --- 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 06cf212204e..8a4daa16d01 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -2,7 +2,7 @@ aiohttp==3.5.4 astral==1.9.2 async_timeout==3.0.1 attrs==18.2.0 -bcrypt==3.1.5 +bcrypt==3.1.6 certifi>=2018.04.16 jinja2>=2.10 PyJWT==1.6.4 diff --git a/requirements_all.txt b/requirements_all.txt index 6d327a817f0..de3db3d424b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3,7 +3,7 @@ aiohttp==3.5.4 astral==1.9.2 async_timeout==3.0.1 attrs==18.2.0 -bcrypt==3.1.5 +bcrypt==3.1.6 certifi>=2018.04.16 jinja2>=2.10 PyJWT==1.6.4 diff --git a/setup.py b/setup.py index 3b2863e9921..5fec6ec3a09 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ REQUIRES = [ 'astral==1.9.2', 'async_timeout==3.0.1', 'attrs==18.2.0', - 'bcrypt==3.1.5', + 'bcrypt==3.1.6', 'certifi>=2018.04.16', 'jinja2>=2.10', 'PyJWT==1.6.4', From 0b68da2f8871f7557e33586ab3f8c90454afd92c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 27 Feb 2019 18:37:26 +0530 Subject: [PATCH 196/253] Upgrade shodan to 1.11.1 (#21478) --- homeassistant/components/sensor/shodan.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py index 234631df5f2..ee64eecf3fe 100644 --- a/homeassistant/components/sensor/shodan.py +++ b/homeassistant/components/sensor/shodan.py @@ -9,7 +9,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['shodan==1.11.0'] +REQUIREMENTS = ['shodan==1.11.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index de3db3d424b..e3e73569ca0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1553,7 +1553,7 @@ sense_energy==0.6.0 sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan -shodan==1.11.0 +shodan==1.11.1 # homeassistant.components.notify.simplepush simplepush==1.1.4 From 27e6c6665f5170669d8c30d70c737478b2758c4a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 27 Feb 2019 18:37:51 +0530 Subject: [PATCH 197/253] Upgrade astral to 1.10.1 (#21474) --- 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 8a4daa16d01..fa1fe5a959d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,5 +1,5 @@ aiohttp==3.5.4 -astral==1.9.2 +astral==1.10.1 async_timeout==3.0.1 attrs==18.2.0 bcrypt==3.1.6 diff --git a/requirements_all.txt b/requirements_all.txt index e3e73569ca0..18b88a42d8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,6 +1,6 @@ # Home Assistant core aiohttp==3.5.4 -astral==1.9.2 +astral==1.10.1 async_timeout==3.0.1 attrs==18.2.0 bcrypt==3.1.6 diff --git a/setup.py b/setup.py index 5fec6ec3a09..c4c9d0e53ed 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ 'aiohttp==3.5.4', - 'astral==1.9.2', + 'astral==1.10.1', 'async_timeout==3.0.1', 'attrs==18.2.0', 'bcrypt==3.1.6', From 9066609d23554dcbb80b1bec95f80c16ac4b9f82 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 27 Feb 2019 08:34:38 -0500 Subject: [PATCH 198/253] Refactor async_turn_on() for ZHA Light. (#21156) * Refactor async_turn_on() for ZHA Light. Use "move_to_level_with_on_off" if brightness or transition attributes are present in the service call data, otherwise issue "On" Zigbee command. Allow brightness of 0 for service call -- effectively turning the light off. Send color commands only after the light was turned on. * Fix zha.light tests. --- homeassistant/components/zha/light.py | 58 +++++++++++-------- tests/components/zha/test_light.py | 83 +++++++++++++++++++-------- 2 files changed, 92 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index efa6f679ae8..740d67db1bd 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['zha'] -DEFAULT_DURATION = 0.5 +DEFAULT_DURATION = 5 CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 @@ -110,8 +110,13 @@ class Light(ZhaEntity, light.Light): return self.state_attributes def set_level(self, value): - """Set the brightness of this light between 0..255.""" - value = max(0, min(255, value)) + """Set the brightness of this light between 0..254. + + brightness level 255 is a special value instructing the device to come + on at `on_level` Zigbee attribute value, regardless of the last set + level + """ + value = max(0, min(254, value)) self._brightness = value self.async_schedule_update_ha_state() @@ -146,8 +151,31 @@ class Light(ZhaEntity, light.Light): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION) - duration = duration * 10 # tenths of s + transition = kwargs.get(light.ATTR_TRANSITION) + duration = transition * 10 if transition else DEFAULT_DURATION + brightness = kwargs.get(light.ATTR_BRIGHTNESS) + + if (brightness is not None or transition) and \ + self._supported_features & light.SUPPORT_BRIGHTNESS: + if brightness is not None: + level = min(254, brightness) + else: + level = self._brightness or 254 + success = await self._level_channel.move_to_level_with_on_off( + level, + duration + ) + if not success: + return + self._state = bool(level) + if level: + self._brightness = level + + if brightness is None or brightness: + success = await self._on_off_channel.on() + if not success: + return + self._state = True if light.ATTR_COLOR_TEMP in kwargs and \ self.supported_features & light.SUPPORT_COLOR_TEMP: @@ -171,32 +199,12 @@ class Light(ZhaEntity, light.Light): return self._hs_color = hs_color - if self._brightness is not None: - brightness = kwargs.get( - light.ATTR_BRIGHTNESS, self._brightness or 255) - success = await self._level_channel.move_to_level_with_on_off( - brightness, - duration - ) - if not success: - return - self._state = True - self._brightness = brightness - self.async_schedule_update_ha_state() - return - - success = await self._on_off_channel.on() - if not success: - return - - self._state = True self.async_schedule_update_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" duration = kwargs.get(light.ATTR_TRANSITION) supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS - success = None if duration and supports_level: success = await self._level_channel.move_to_level_with_on_off( 0, diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 38d7caedaad..0ccad52d6aa 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1,20 +1,24 @@ """Test zha light.""" -from unittest.mock import call, patch +import asyncio +from unittest.mock import MagicMock, call, patch, sentinel + from homeassistant.components.light import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE -from tests.common import mock_coro +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE + from .common import ( - async_init_zigpy_device, make_attribute, make_entity_id, - async_test_device_join, async_enable_traffic -) + async_enable_traffic, async_init_zigpy_device, async_test_device_join, + make_attribute, make_entity_id) + +from tests.common import mock_coro ON = 1 OFF = 0 -async def test_light(hass, config_entry, zha_gateway): +async def test_light(hass, config_entry, zha_gateway, monkeypatch): """Test zha light platform.""" from zigpy.zcl.clusters.general import OnOff, LevelControl, Basic + from zigpy.zcl.foundation import Status from zigpy.profiles.zha import DeviceType # create zigpy devices @@ -52,6 +56,12 @@ async def test_light(hass, config_entry, zha_gateway): # dimmable light level_device_on_off_cluster = zigpy_device_level.endpoints.get(1).on_off level_device_level_cluster = zigpy_device_level.endpoints.get(1).level + on_off_mock = MagicMock(side_effect=asyncio.coroutine(MagicMock( + return_value=(sentinel.data, Status.SUCCESS)))) + level_mock = MagicMock(side_effect=asyncio.coroutine(MagicMock( + return_value=(sentinel.data, Status.SUCCESS)))) + monkeypatch.setattr(level_device_on_off_cluster, 'request', on_off_mock) + monkeypatch.setattr(level_device_level_cluster, 'request', level_mock) level_entity_id = make_entity_id(DOMAIN, zigpy_device_level, level_device_on_off_cluster, use_suffix=False) @@ -81,7 +91,8 @@ async def test_light(hass, config_entry, zha_gateway): hass, on_off_device_on_off_cluster, on_off_entity_id) await async_test_level_on_off_from_hass( - hass, level_device_on_off_cluster, level_entity_id) + hass, level_device_on_off_cluster, level_device_level_cluster, + level_entity_id) # test turning the lights on and off from the light await async_test_on_from_light( @@ -131,7 +142,7 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id): await hass.services.async_call(DOMAIN, 'turn_on', { 'entity_id': entity_id }, blocking=True) - assert len(cluster.request.mock_calls) == 1 + assert cluster.request.call_count == 1 assert cluster.request.call_args == call( False, ON, (), expect_reply=True, manufacturer=None) @@ -148,28 +159,52 @@ async def async_test_off_from_hass(hass, cluster, entity_id): await hass.services.async_call(DOMAIN, 'turn_off', { 'entity_id': entity_id }, blocking=True) - assert len(cluster.request.mock_calls) == 1 + assert cluster.request.call_count == 1 assert cluster.request.call_args == call( False, OFF, (), expect_reply=True, manufacturer=None) -async def async_test_level_on_off_from_hass(hass, cluster, entity_id): +async def async_test_level_on_off_from_hass(hass, on_off_cluster, + level_cluster, entity_id): """Test on off functionality from hass.""" from zigpy import types - from zigpy.zcl.foundation import Status - with patch( - 'zigpy.zcl.Cluster.request', - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])): - # turn on via UI - await hass.services.async_call(DOMAIN, 'turn_on', { - 'entity_id': entity_id - }, blocking=True) - assert len(cluster.request.mock_calls) == 1 - assert cluster.request.call_args == call( - False, 4, (types.uint8_t, types.uint16_t), 255, 5.0, - expect_reply=True, manufacturer=None) + # turn on via UI + await hass.services.async_call(DOMAIN, 'turn_on', {'entity_id': entity_id}, + blocking=True) + assert on_off_cluster.request.call_count == 1 + assert level_cluster.request.call_count == 0 + assert on_off_cluster.request.call_args == call( + False, 1, (), expect_reply=True, manufacturer=None) + on_off_cluster.request.reset_mock() + level_cluster.request.reset_mock() - await async_test_off_from_hass(hass, cluster, entity_id) + await hass.services.async_call(DOMAIN, 'turn_on', + {'entity_id': entity_id, 'transition': 10}, + blocking=True) + assert on_off_cluster.request.call_count == 1 + assert level_cluster.request.call_count == 1 + assert on_off_cluster.request.call_args == call( + False, 1, (), expect_reply=True, manufacturer=None) + assert level_cluster.request.call_args == call( + False, 4, (types.uint8_t, types.uint16_t), 254, 100.0, + expect_reply=True, manufacturer=None) + on_off_cluster.request.reset_mock() + level_cluster.request.reset_mock() + + await hass.services.async_call(DOMAIN, 'turn_on', + {'entity_id': entity_id, 'brightness': 10}, + blocking=True) + assert on_off_cluster.request.call_count == 1 + assert level_cluster.request.call_count == 1 + assert on_off_cluster.request.call_args == call( + False, 1, (), expect_reply=True, manufacturer=None) + assert level_cluster.request.call_args == call( + False, 4, (types.uint8_t, types.uint16_t), 10, 5.0, + expect_reply=True, manufacturer=None) + on_off_cluster.request.reset_mock() + level_cluster.request.reset_mock() + + await async_test_off_from_hass(hass, on_off_cluster, entity_id) async def async_test_dimmer_from_light(hass, cluster, entity_id, From 9b3a3fc1acf276db8e0f4b9535ff82a6f99b30b1 Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Wed, 27 Feb 2019 20:25:11 +0100 Subject: [PATCH 199/253] Add device_info to enable HA-devices for Homematic IP (#21241) * add device_info to device * added checks * Fixes based on feedback * Fix spelling * Simplified implementation On homematicip devices and the ap are created * small fix with device.id * hub/ap device creation moved to __init__.py * Fixed result handling * fixes after review. * Fix test --- .../components/homematicip_cloud/__init__.py | 18 ++++++++++++++++- .../components/homematicip_cloud/device.py | 20 +++++++++++++++++++ .../components/homematicip_cloud/sensor.py | 11 ++++++++++ .../components/homematicip_cloud/test_init.py | 6 +++--- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index b45ac291bfc..fd07356d7fb 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_NAME +from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from .config_flow import configured_haps @@ -52,7 +53,22 @@ async def async_setup_entry(hass, entry): hap = HomematicipHAP(hass, entry) hapid = entry.data[HMIPC_HAPID].replace('-', '').upper() hass.data[DOMAIN][hapid] = hap - return await hap.async_setup() + + if not await hap.async_setup(): + return False + + # Register hap as device in registry. + device_registry = await dr.async_get_registry(hass) + home = hap.home + device_registry.async_get_or_create( + config_entry_id=home.id, + identifiers={(DOMAIN, home.id)}, + manufacturer='eQ-3', + name=home.label, + model=home.modelType, + sw_version=home.currentAPVersion, + ) + return True async def async_unload_entry(hass, entry): diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index c43f0e24e2b..85cc3c0c77a 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -1,6 +1,7 @@ """Generic device for the HomematicIP Cloud component.""" import logging +from homeassistant.components import homematicip_cloud from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -32,6 +33,25 @@ class HomematicipGenericDevice(Entity): self.post = post _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) + @property + def device_info(self): + """Return device specific attributes.""" + from homematicip.aio.device import AsyncDevice + # Only physical devices should be HA devices. + if isinstance(self._device, AsyncDevice): + return { + 'identifiers': { + # Serial numbers of Homematic IP device + (homematicip_cloud.DOMAIN, self._device.id) + }, + 'name': self._device.label, + 'manufacturer': self._device.oem, + 'model': self._device.modelType, + 'sw_version': self._device.firmwareVersion, + 'via_hub': (homematicip_cloud.DOMAIN, self._device.homeId), + } + return None + async def async_added_to_hass(self): """Register callbacks.""" self._device.on_update(self._device_changed) diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index b753a4d1aa5..d755735e0e0 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -65,6 +65,17 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): """Initialize access point device.""" super().__init__(home, home) + @property + def device_info(self): + """Return device specific attributes.""" + # Adds a sensor to the existing HAP device + return { + 'identifiers': { + # Serial numbers of Homematic IP device + (HMIPC_DOMAIN, self._device.id) + } + } + @property def icon(self): """Return the icon of the access point device.""" diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 18537227247..8b02b36de20 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -21,7 +21,7 @@ async def test_config_with_accesspoint_passed_to_config_entry(hass): }) is True # Flow started for the access point - assert len(mock_config_entries.flow.mock_calls) == 2 + assert len(mock_config_entries.flow.mock_calls) >= 2 async def test_config_already_registered_not_passed_to_config_entry(hass): @@ -58,7 +58,7 @@ async def test_setup_entry_successful(hass): } }) is True - assert len(mock_hap.mock_calls) == 2 + assert len(mock_hap.mock_calls) >= 2 async def test_setup_defined_accesspoint(hass): @@ -95,7 +95,7 @@ async def test_unload_entry(hass): mock_hap.return_value.async_setup.return_value = mock_coro(True) assert await async_setup_component(hass, hmipc.DOMAIN, {}) is True - assert len(mock_hap.return_value.mock_calls) == 1 + assert len(mock_hap.return_value.mock_calls) >= 1 mock_hap.return_value.async_reset.return_value = mock_coro(True) assert await hmipc.async_unload_entry(hass, entry) From b87eb9d79e2d481e8c6727a8bb5b30bc08e20f8e Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Thu, 28 Feb 2019 03:33:34 +0800 Subject: [PATCH 200/253] Fire events when Google Assistant commands come in #15139 (#20204) --- .../components/google_assistant/__init__.py | 2 + .../components/google_assistant/const.py | 5 + .../components/google_assistant/smart_home.py | 35 ++++-- .../google_assistant/test_smart_home.py | 101 +++++++++++++++++- 4 files changed, 134 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index f5ca9b8b146..0fd167c2729 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -22,6 +22,8 @@ from .const import ( CONF_EXPOSE, CONF_ALIASES, CONF_ROOM_HINT, CONF_ALLOW_UNLOCK, DEFAULT_ALLOW_UNLOCK ) +from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401 +from .const import EVENT_QUERY_RECEIVED # noqa: F401 from .http import async_register_http _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index bfeb0fcadf5..b7d3a398ef2 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -42,3 +42,8 @@ ERR_NOT_SUPPORTED = "notSupported" ERR_PROTOCOL_ERROR = 'protocolError' ERR_UNKNOWN_ERROR = 'unknownError' ERR_FUNCTION_NOT_SUPPORTED = 'functionNotSupported' + +# Event types +EVENT_COMMAND_RECEIVED = 'google_assistant_command_received' +EVENT_QUERY_RECEIVED = 'google_assistant_query_received' +EVENT_SYNC_RECEIVED = 'google_assistant_sync_received' diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 8ea7a8aa7bc..21316c62085 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -8,7 +8,7 @@ from homeassistant.util.decorator import Registry from homeassistant.core import callback from homeassistant.const import ( CLOUD_NEVER_EXPOSED_ENTITIES, CONF_NAME, STATE_UNAVAILABLE, - ATTR_SUPPORTED_FEATURES + ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, ) from homeassistant.components import ( climate, @@ -32,7 +32,8 @@ from .const import ( TYPE_THERMOSTAT, TYPE_FAN, CONF_ALIASES, CONF_ROOM_HINT, ERR_FUNCTION_NOT_SUPPORTED, ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, - ERR_UNKNOWN_ERROR + ERR_UNKNOWN_ERROR, + EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED ) from .helpers import SmartHomeError @@ -214,7 +215,8 @@ async def _process(hass, config, message): } try: - result = await handler(hass, config, inputs[0].get('payload')) + result = await handler(hass, config, request_id, + inputs[0].get('payload')) except SmartHomeError as err: return { 'requestId': request_id, @@ -233,11 +235,15 @@ async def _process(hass, config, message): @HANDLERS.register('action.devices.SYNC') -async def async_devices_sync(hass, config, payload): +async def async_devices_sync(hass, config, request_id, payload): """Handle action.devices.SYNC request. https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ + hass.bus.async_fire(EVENT_SYNC_RECEIVED, { + 'request_id': request_id + }) + devices = [] for state in hass.states.async_all(): if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: @@ -255,14 +261,16 @@ async def async_devices_sync(hass, config, payload): devices.append(serialized) - return { + response = { 'agentUserId': config.agent_user_id, 'devices': devices, } + return response + @HANDLERS.register('action.devices.QUERY') -async def async_devices_query(hass, config, payload): +async def async_devices_query(hass, config, request_id, payload): """Handle action.devices.QUERY request. https://developers.google.com/actions/smarthome/create-app#actiondevicesquery @@ -272,6 +280,11 @@ async def async_devices_query(hass, config, payload): devid = device['id'] state = hass.states.get(devid) + hass.bus.async_fire(EVENT_QUERY_RECEIVED, { + 'request_id': request_id, + ATTR_ENTITY_ID: devid, + }) + if not state: # If we can't find a state, the device is offline devices[devid] = {'online': False} @@ -283,7 +296,7 @@ async def async_devices_query(hass, config, payload): @HANDLERS.register('action.devices.EXECUTE') -async def handle_devices_execute(hass, config, payload): +async def handle_devices_execute(hass, config, request_id, payload): """Handle action.devices.EXECUTE request. https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute @@ -296,6 +309,12 @@ async def handle_devices_execute(hass, config, payload): command['execution']): entity_id = device['id'] + hass.bus.async_fire(EVENT_COMMAND_RECEIVED, { + 'request_id': request_id, + ATTR_ENTITY_ID: entity_id, + 'execution': execution + }) + # Happens if error occurred. Skip entity for further processing if entity_id in results: continue @@ -341,7 +360,7 @@ async def handle_devices_execute(hass, config, payload): @HANDLERS.register('action.devices.DISCONNECT') -async def async_devices_disconnect(hass, config, payload): +async def async_devices_disconnect(hass, config, request_id, payload): """Handle action.devices.DISCONNECT request. https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 05ae0809527..d1ec80844b6 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -7,7 +7,8 @@ from homeassistant.components.climate.const import ( ATTR_MIN_TEMP, ATTR_MAX_TEMP, STATE_HEAT, SUPPORT_OPERATION_MODE ) from homeassistant.components.google_assistant import ( - const, trait, helpers, smart_home as sh) + const, trait, helpers, smart_home as sh, + EVENT_COMMAND_RECEIVED, EVENT_QUERY_RECEIVED, EVENT_SYNC_RECEIVED) from homeassistant.components.light.demo import DemoLight @@ -48,6 +49,9 @@ async def test_sync_message(hass): } ) + events = [] + hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append) + result = await sh.async_handle_message(hass, config, { "requestId": REQ_ID, "inputs": [{ @@ -85,6 +89,13 @@ async def test_sync_message(hass): }] } } + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].event_type == EVENT_SYNC_RECEIVED + assert events[0].data == { + 'request_id': REQ_ID, + } async def test_query_message(hass): @@ -109,6 +120,9 @@ async def test_query_message(hass): light2.entity_id = 'light.another_light' await light2.async_update_ha_state() + events = [] + hass.bus.async_listen(EVENT_QUERY_RECEIVED, events.append) + result = await sh.async_handle_message(hass, BASIC_CONFIG, { "requestId": REQ_ID, "inputs": [{ @@ -149,12 +163,33 @@ async def test_query_message(hass): } } + assert len(events) == 3 + assert events[0].event_type == EVENT_QUERY_RECEIVED + assert events[0].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.demo_light' + } + assert events[1].event_type == EVENT_QUERY_RECEIVED + assert events[1].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.another_light' + } + assert events[2].event_type == EVENT_QUERY_RECEIVED + assert events[2].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.non_existing' + } + async def test_execute(hass): """Test an execute command.""" await async_setup_component(hass, 'light', { 'light': {'platform': 'demo'} }) + + events = [] + hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append) + await hass.services.async_call( 'light', 'turn_off', {'entity_id': 'light.ceiling_lights'}, blocking=True) @@ -209,6 +244,52 @@ async def test_execute(hass): } } + assert len(events) == 4 + assert events[0].event_type == EVENT_COMMAND_RECEIVED + assert events[0].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.non_existing', + 'execution': { + 'command': 'action.devices.commands.OnOff', + 'params': { + 'on': True + } + } + } + assert events[1].event_type == EVENT_COMMAND_RECEIVED + assert events[1].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.non_existing', + 'execution': { + 'command': 'action.devices.commands.BrightnessAbsolute', + 'params': { + 'brightness': 20 + } + } + } + assert events[2].event_type == EVENT_COMMAND_RECEIVED + assert events[2].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.ceiling_lights', + 'execution': { + 'command': 'action.devices.commands.OnOff', + 'params': { + 'on': True + } + } + } + assert events[3].event_type == EVENT_COMMAND_RECEIVED + assert events[3].data == { + 'request_id': REQ_ID, + 'entity_id': 'light.ceiling_lights', + 'execution': { + 'command': 'action.devices.commands.BrightnessAbsolute', + 'params': { + 'brightness': 20 + } + } + } + async def test_raising_error_trait(hass): """Test raising an error while executing a trait command.""" @@ -218,6 +299,11 @@ async def test_raising_error_trait(hass): ATTR_SUPPORTED_FEATURES: SUPPORT_OPERATION_MODE, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }) + + events = [] + hass.bus.async_listen(EVENT_COMMAND_RECEIVED, events.append) + await hass.async_block_till_done() + result = await sh.async_handle_message(hass, BASIC_CONFIG, { "requestId": REQ_ID, "inputs": [{ @@ -250,6 +336,19 @@ async def test_raising_error_trait(hass): } } + assert len(events) == 1 + assert events[0].event_type == EVENT_COMMAND_RECEIVED + assert events[0].data == { + 'request_id': REQ_ID, + 'entity_id': 'climate.bla', + 'execution': { + 'command': 'action.devices.commands.ThermostatTemperatureSetpoint', + 'params': { + 'thermostatTemperatureSetpoint': 10 + } + } + } + def test_serialize_input_boolean(): """Test serializing an input boolean entity.""" From 2482816a11afe51d09eedae5485345f2cfb4ce29 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 27 Feb 2019 21:04:55 +0100 Subject: [PATCH 201/253] Fix deCONZ retry mechanism for setup --- homeassistant/components/deconz/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index e640e9ba896..d107cba8f7b 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -68,11 +68,11 @@ async def async_setup_entry(hass, config_entry): gateway = DeconzGateway(hass, config_entry) - hass.data[DOMAIN] = gateway - if not await gateway.async_setup(): return False + hass.data[DOMAIN] = gateway + device_registry = await \ hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( From 519315f9c82ba9f0ea0bdcc622b4952b1c0a605c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 27 Feb 2019 22:10:40 +0100 Subject: [PATCH 202/253] pylint 2.3.0 (#21485) * pylint 2.3.0 * remove const * disable=syntax-error --- homeassistant/auth/__init__.py | 3 +-- homeassistant/components/bloomsky/__init__.py | 2 +- .../components/device_tracker/__init__.py | 2 +- homeassistant/components/device_tracker/ubus.py | 3 +-- homeassistant/components/logbook/__init__.py | 3 +-- homeassistant/components/media_player/bluesound.py | 5 ++--- homeassistant/components/nest/local_auth.py | 5 ++--- homeassistant/components/notify/nfandroidtv.py | 2 +- homeassistant/components/notify/pushsafer.py | 3 +-- homeassistant/components/notify/slack.py | 2 +- homeassistant/components/panel_custom/__init__.py | 2 +- homeassistant/components/python_script/__init__.py | 14 +++++++------- homeassistant/components/recorder/util.py | 3 +-- homeassistant/components/sensor/currencylayer.py | 7 +++---- homeassistant/components/sensor/starlingbank.py | 2 +- homeassistant/components/switch/switchmate.py | 4 ++-- homeassistant/components/telegram_bot/__init__.py | 8 ++++---- homeassistant/components/telegram_bot/polling.py | 2 +- .../components/tellduslive/config_flow.py | 3 +-- homeassistant/components/websocket_api/http.py | 2 +- homeassistant/helpers/area_registry.py | 4 ++-- homeassistant/helpers/config_validation.py | 4 ++-- homeassistant/helpers/entity.py | 9 ++++----- homeassistant/helpers/entity_platform.py | 6 +++--- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 26 files changed, 47 insertions(+), 57 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 3377bb2a6aa..bb90f296468 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -170,8 +170,7 @@ class AuthManager: user = await self.async_get_user_by_credentials(credentials) if user is None: raise ValueError('Unable to find the user.') - else: - return user + return user auth_provider = self._async_get_auth_provider(credentials) diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py index a42eb34004b..7f924929662 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -66,7 +66,7 @@ class BloomSky: self.API_URL, headers={AUTHORIZATION: self._api_key}, timeout=10) if response.status_code == 401: raise RuntimeError("Invalid API_KEY") - elif response.status_code != 200: + if response.status_code != 200: _LOGGER.error("Invalid HTTP response: %s", response.status_code) return # Create dictionary keyed off of the device unique id diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 7d8449197de..1263811aae7 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -291,7 +291,7 @@ class DeviceTracker: """ if mac is None and dev_id is None: raise HomeAssistantError('Neither mac or device id passed in') - elif mac is not None: + if mac is not None: mac = str(mac).upper() device = self.mac_to_dev.get(mac) if not device: diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index 94e3b407d13..96f2f60c1e5 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -216,8 +216,7 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params): if 'message' in response['error'] and \ response['error']['message'] == "Access denied": raise PermissionError(response['error']['message']) - else: - raise HomeAssistantError(response['error']['message']) + raise HomeAssistantError(response['error']['message']) if rpcmethod == "call": try: diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 74a90f0f5f0..dbedc8c6d70 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -366,8 +366,7 @@ def _get_related_entity_ids(session, entity_filter): if tryno == RETRIES - 1: raise - else: - time.sleep(QUERY_RETRY_WAIT) + time.sleep(QUERY_RETRY_WAIT) def _generate_filter_from_config(config): diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index c6a8c51ca58..b25916c7f66 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -345,9 +345,8 @@ class BluesoundPlayer(MediaPlayerDevice): if raise_timeout: _LOGGER.info("Timeout: %s", self.host) raise - else: - _LOGGER.debug("Failed communicating: %s", self.host) - return None + _LOGGER.debug("Failed communicating: %s", self.host) + return None return data diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py index 393a36e4a9c..5cb63956aea 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/local_auth.py @@ -41,6 +41,5 @@ async def resolve_auth_code(hass, client_id, client_secret, code): except AuthorizationError as err: if err.response.status_code == 401: raise config_flow.CodeInvalid() - else: - raise config_flow.NestAuthError('Unknown error: {} ({})'.format( - err, err.response.status_code)) + raise config_flow.NestAuthError('Unknown error: {} ({})'.format( + err, err.response.status_code)) diff --git a/homeassistant/components/notify/nfandroidtv.py b/homeassistant/components/notify/nfandroidtv.py index faf5e90e016..f99d97574b4 100644 --- a/homeassistant/components/notify/nfandroidtv.py +++ b/homeassistant/components/notify/nfandroidtv.py @@ -248,7 +248,7 @@ class NFAndroidTVNotificationService(BaseNotificationService): req = requests.get(url, timeout=DEFAULT_TIMEOUT) return req.content - elif local_path is not None: + if local_path is not None: # Check whether path is whitelisted in configuration.yaml if self.is_allowed_path(local_path): return open(local_path, "rb") diff --git a/homeassistant/components/notify/pushsafer.py b/homeassistant/components/notify/pushsafer.py index 443d56521c1..94dc08a8113 100644 --- a/homeassistant/components/notify/pushsafer.py +++ b/homeassistant/components/notify/pushsafer.py @@ -149,8 +149,7 @@ class PushsaferNotificationService(BaseNotificationService): response = requests.get(url, timeout=CONF_TIMEOUT) return self.get_base64(response.content, response.headers['content-type']) - else: - _LOGGER.warning("url not found in param") + _LOGGER.warning("url not found in param") return None diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 8e23c9f4fa0..961f671203f 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -152,7 +152,7 @@ class SlackNotificationService(BaseNotificationService): req = requests.get(url, timeout=CONF_TIMEOUT) return req.content - elif local_path: + if local_path: # Check whether path is whitelisted in configuration.yaml if self.is_allowed_path(local_path): return open(local_path, 'rb') diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index f6602169eb2..2fce5d9857c 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -81,7 +81,7 @@ async def async_register_panel( """Register a new custom panel.""" if js_url is None and html_url is None and module_url is None: raise ValueError('Either js_url, module_url or html_url is required.') - elif (js_url and html_url) or (module_url and html_url): + if (js_url and html_url) or (module_url and html_url): raise ValueError('Pass in only one of JS url, Module url or HTML url.') if config is not None and not isinstance(config, dict): diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 3d0952b89fb..d639b638033 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -125,13 +125,13 @@ def execute(hass, filename, source, data=None): # pylint: disable=too-many-boolean-expressions if name.startswith('async_'): raise ScriptError("Not allowed to access async methods") - elif (obj is hass and name not in ALLOWED_HASS or - obj is hass.bus and name not in ALLOWED_EVENTBUS or - obj is hass.states and name not in ALLOWED_STATEMACHINE or - obj is hass.services and name not in ALLOWED_SERVICEREGISTRY or - obj is dt_util and name not in ALLOWED_DT_UTIL or - obj is datetime and name not in ALLOWED_DATETIME or - isinstance(obj, TimeWrapper) and name not in ALLOWED_TIME): + if (obj is hass and name not in ALLOWED_HASS or + obj is hass.bus and name not in ALLOWED_EVENTBUS or + obj is hass.states and name not in ALLOWED_STATEMACHINE or + obj is hass.services and name not in ALLOWED_SERVICEREGISTRY or + obj is dt_util and name not in ALLOWED_DT_UTIL or + obj is datetime and name not in ALLOWED_DATETIME or + isinstance(obj, TimeWrapper) and name not in ALLOWED_TIME): raise ScriptError("Not allowed to access {}.{}".format( obj.__class__.__name__, name)) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index c6390e5d8e2..449f910fda9 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -76,5 +76,4 @@ def execute(qry): if tryno == RETRIES - 1: raise - else: - time.sleep(QUERY_RETRY_WAIT) + time.sleep(QUERY_RETRY_WAIT) diff --git a/homeassistant/components/sensor/currencylayer.py b/homeassistant/components/sensor/currencylayer.py index bbc63633c51..9b7186e8e09 100644 --- a/homeassistant/components/sensor/currencylayer.py +++ b/homeassistant/components/sensor/currencylayer.py @@ -119,10 +119,9 @@ class CurrencylayerData: self._resource, params=self._parameters, timeout=10) if 'error' in result.json(): raise ValueError(result.json()['error']['info']) - else: - self.data = result.json()['quotes'] - _LOGGER.debug("Currencylayer data updated: %s", - result.json()['timestamp']) + self.data = result.json()['quotes'] + _LOGGER.debug("Currencylayer data updated: %s", + result.json()['timestamp']) except ValueError as err: _LOGGER.error("Check Currencylayer API %s", err.args) self.data = None diff --git a/homeassistant/components/sensor/starlingbank.py b/homeassistant/components/sensor/starlingbank.py index 84b30daa2e9..9cb57670740 100644 --- a/homeassistant/components/sensor/starlingbank.py +++ b/homeassistant/components/sensor/starlingbank.py @@ -44,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Sterling Bank sensor platform.""" - from starlingbank import StarlingAccount + from starlingbank import StarlingAccount # pylint: disable=syntax-error sensors = [] for account in config[CONF_ACCOUNTS]: diff --git a/homeassistant/components/switch/switchmate.py b/homeassistant/components/switch/switchmate.py index 5289ca6ec1b..60497e0207b 100644 --- a/homeassistant/components/switch/switchmate.py +++ b/homeassistant/components/switch/switchmate.py @@ -34,10 +34,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None) -> None: name = config.get(CONF_NAME) mac_addr = config[CONF_MAC] flip_on_off = config[CONF_FLIP_ON_OFF] - add_entities([Switchmate(mac_addr, name, flip_on_off)], True) + add_entities([SwitchmateEntity(mac_addr, name, flip_on_off)], True) -class Switchmate(SwitchDevice): +class SwitchmateEntity(SwitchDevice): """Representation of a Switchmate.""" def __init__(self, mac, name, flip_on_off) -> None: diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 18f206541df..c55b27e97a6 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -628,7 +628,7 @@ class BaseTelegramBotEntity: self.hass.bus.async_fire(event, event_data) return True - elif ATTR_CALLBACK_QUERY in data: + if ATTR_CALLBACK_QUERY in data: event = EVENT_TELEGRAM_CALLBACK data = data.get(ATTR_CALLBACK_QUERY) message_ok, event_data = self._get_message_data(data) @@ -642,6 +642,6 @@ class BaseTelegramBotEntity: self.hass.bus.async_fire(event, event_data) return True - else: - _LOGGER.warning("Message with unknown data received: %s", data) - return True + + _LOGGER.warning("Message with unknown data received: %s", data) + return True diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 5bca4321a5f..9936b690985 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -61,7 +61,7 @@ def message_handler(handler): """Initialize the messages handler instance.""" super().__init__(handler) - def check_update(self, update): + def check_update(self, update): # pylint: disable=no-self-use """Check is update valid.""" return isinstance(update, Update) diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index 3373e9cc2f7..62463bc0a9e 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -84,8 +84,7 @@ class FlowHandler(config_entries.ConfigFlow): KEY_SCAN_INTERVAL: self._scan_interval.seconds, KEY_SESSION: session, }) - else: - errors['base'] = 'auth_error' + errors['base'] = 'auth_error' try: with async_timeout.timeout(10): diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 42c2c0a5751..1ab2b09d7fa 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -130,7 +130,7 @@ class WebSocketHandler: if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING): raise Disconnect - elif msg.type != WSMsgType.TEXT: + if msg.type != WSMsgType.TEXT: disconnect_warn = 'Received non-Text message.' raise Disconnect diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index bc8c05ed0a6..3fa820f8350 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -75,8 +75,8 @@ class AreaRegistry: if self._async_is_registered(name): raise ValueError('Name is already in use') - else: - changes['name'] = name + + changes['name'] = name new = self.areas[area_id] = attr.evolve(old, **changes) self.async_schedule_save() diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index b5716431217..4bba80aa154 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -293,7 +293,7 @@ def time_period_str(value: str) -> timedelta: """Validate and transform time offset.""" if isinstance(value, int): raise vol.Invalid('Make sure you wrap time values in quotes') - elif not isinstance(value, str): + if not isinstance(value, str): raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) negative_offset = False @@ -440,7 +440,7 @@ def template(value): """Validate a jinja2 template.""" if value is None: raise vol.Invalid('template value is None') - elif isinstance(value, (list, dict, template_helper.Template)): + if isinstance(value, (list, dict, template_helper.Template)): raise vol.Invalid('template value should be a string') value = template_helper.Template(str(value)) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index c13ebe7cfab..dd9677f6515 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -28,11 +28,10 @@ def generate_entity_id(entity_id_format: str, name: Optional[str], if current_ids is None: if hass is None: raise ValueError("Missing required parameter currentids or hass") - else: - return run_callback_threadsafe( - hass.loop, async_generate_entity_id, entity_id_format, name, - current_ids, hass - ).result() + return run_callback_threadsafe( + hass.loop, async_generate_entity_id, entity_id_format, name, + current_ids, hass + ).result() name = (slugify(name) or slugify(DEVICE_DEFAULT_NAME)).lower() diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 9c76d244138..87cc4d4fd90 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -334,9 +334,9 @@ class EntityPlatform: if not valid_entity_id(entity.entity_id): raise HomeAssistantError( 'Invalid entity id: {}'.format(entity.entity_id)) - elif (entity.entity_id in self.entities or - entity.entity_id in self.hass.states.async_entity_ids( - self.domain)): + if (entity.entity_id in self.entities or + entity.entity_id in self.hass.states.async_entity_ids( + self.domain)): msg = 'Entity id already exists: {}'.format(entity.entity_id) if entity.unique_id is not None: msg += '. Platform {} does not generate unique IDs'.format( diff --git a/requirements_test.txt b/requirements_test.txt index 6568ca89a07..531fb0b78f6 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ flake8==3.7.7 mock-open==1.3.1 mypy==0.670 pydocstyle==3.0.0 -pylint==2.2.2 +pylint==2.3.0 pytest-aiohttp==0.3.0 pytest-cov==2.6.1 pytest-sugar==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 493888baeb9..267a1006d32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -9,7 +9,7 @@ flake8==3.7.7 mock-open==1.3.1 mypy==0.670 pydocstyle==3.0.0 -pylint==2.2.2 +pylint==2.3.0 pytest-aiohttp==0.3.0 pytest-cov==2.6.1 pytest-sugar==0.9.2 From 8a026bf2146e7196bd26db03736fd22ffc387c26 Mon Sep 17 00:00:00 2001 From: CV Date: Wed, 27 Feb 2019 23:09:49 +0100 Subject: [PATCH 203/253] HomeMatic: Add error-attribute (#21009) Error attribute has different meanings depending on device --- homeassistant/components/homematic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 4a27d918d95..dba4add216d 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -104,7 +104,7 @@ HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { HM_ATTRIBUTE_SUPPORT = { 'LOWBAT': ['battery', {0: 'High', 1: 'Low'}], 'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}], - 'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}], + 'ERROR': ['error', {0: 'No'}], 'ERROR_SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], 'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], 'RSSI_PEER': ['rssi_peer', {}], From 0ebd12fa6cdbec2c521f1ce92a4e3707ba00d314 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 27 Feb 2019 16:08:02 -0800 Subject: [PATCH 204/253] Avoid unnecessary commands in Vizio update function (#20867) --- .../components/media_player/vizio.py | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/media_player/vizio.py b/homeassistant/components/media_player/vizio.py index 395f5bb369e..af3fdd1e15a 100644 --- a/homeassistant/components/media_player/vizio.py +++ b/homeassistant/components/media_player/vizio.py @@ -92,25 +92,31 @@ class VizioDevice(MediaPlayerDevice): def update(self): """Retrieve latest state of the TV.""" is_on = self._device.get_power_state() - if is_on is None: - self._state = None - return - if is_on is False: - self._state = STATE_OFF - else: + + if is_on: self._state = STATE_ON - 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 - inputs = self._device.get_inputs() - if inputs is not None: - self._available_inputs = [] - for input_ in inputs: - self._available_inputs.append(input_.name) + 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 + + inputs = self._device.get_inputs() + if inputs is not None: + self._available_inputs = [input_.name for input_ in inputs] + + else: + if is_on is None: + self._state = None + else: + self._state = STATE_OFF + + self._volume_level = None + self._current_input = None + self._available_inputs = None @property def state(self): From 3749321fa5ad8413222ed1fe636363759a92ed8e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 27 Feb 2019 17:26:59 -0700 Subject: [PATCH 205/253] Upgrade pyairvisual to 3.0.1 (#21512) --- homeassistant/components/sensor/airvisual.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py index ff99dce5e06..46457a17ebb 100644 --- a/homeassistant/components/sensor/airvisual.py +++ b/homeassistant/components/sensor/airvisual.py @@ -18,7 +18,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['pyairvisual==2.0.1'] +REQUIREMENTS = ['pyairvisual==3.0.1'] _LOGGER = getLogger(__name__) ATTR_CITY = 'city' diff --git a/requirements_all.txt b/requirements_all.txt index 18b88a42d8d..ef86be28d5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -927,7 +927,7 @@ pyads==3.0.7 pyaftership==0.1.2 # homeassistant.components.sensor.airvisual -pyairvisual==2.0.1 +pyairvisual==3.0.1 # homeassistant.components.alarm_control_panel.alarmdotcom pyalarmdotcom==0.3.2 From e4b2aab1e94293236b67572f2cf48bf2acd28870 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 27 Feb 2019 17:27:13 -0700 Subject: [PATCH 206/253] Bump simplisafe-python to 3.4.1 (#21511) --- homeassistant/components/simplisafe/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index fcd9d15839b..f494ccf390e 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv from .config_flow import configured_instances from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE -REQUIREMENTS = ['simplisafe-python==3.1.14'] +REQUIREMENTS = ['simplisafe-python==3.4.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ef86be28d5c..f3293a5625a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1559,7 +1559,7 @@ shodan==1.11.1 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==3.1.14 +simplisafe-python==3.4.1 # homeassistant.components.sisyphus sisyphus-control==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 267a1006d32..807084838fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -269,7 +269,7 @@ ring_doorbell==0.2.2 rxv==0.6.0 # homeassistant.components.simplisafe -simplisafe-python==3.1.14 +simplisafe-python==3.4.1 # homeassistant.components.sleepiq sleepyq==0.6 From ab2be6df48f7e60933ab1a3f0aa5315b33b77ed9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 27 Feb 2019 17:27:26 -0700 Subject: [PATCH 207/253] Upgrade aioambient to 0.1.3 (#21510) --- homeassistant/components/ambient_station/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 16b86a0e298..a8e1c7fb292 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -20,7 +20,7 @@ from .const import ( ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE, TYPE_BINARY_SENSOR, TYPE_SENSOR) -REQUIREMENTS = ['aioambient==0.1.2'] +REQUIREMENTS = ['aioambient==0.1.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f3293a5625a..26882cace9c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -90,7 +90,7 @@ abodepy==0.15.0 afsapi==0.0.4 # homeassistant.components.ambient_station -aioambient==0.1.2 +aioambient==0.1.3 # homeassistant.components.asuswrt aioasuswrt==1.1.20 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 807084838fc..e672549c110 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -31,7 +31,7 @@ PyTransportNSW==0.1.1 YesssSMS==0.2.3 # homeassistant.components.ambient_station -aioambient==0.1.2 +aioambient==0.1.3 # homeassistant.components.device_tracker.automatic aioautomatic==0.6.5 From 732110b4c3218c346327f267cf04c6c335204a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 28 Feb 2019 01:33:26 +0100 Subject: [PATCH 208/253] Upgrade tibber lib (#21486) --- homeassistant/components/tibber/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index ba9ae43f13b..f254774eea4 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -11,7 +11,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession -REQUIREMENTS = ['pyTibber==0.9.4'] +REQUIREMENTS = ['pyTibber==0.9.6'] DOMAIN = 'tibber' diff --git a/requirements_all.txt b/requirements_all.txt index 26882cace9c..2354019908e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -909,7 +909,7 @@ pyRFXtrx==0.23 # pySwitchmate==0.4.5 # homeassistant.components.tibber -pyTibber==0.9.4 +pyTibber==0.9.6 # homeassistant.components.switch.dlink pyW215==0.6.0 From f3bb9e870e11dc5ec61ca6d260005bb68d841d20 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Feb 2019 16:33:56 -0800 Subject: [PATCH 209/253] Updated frontend to 20190227.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 caf6bbccb5c..7ef030e87d7 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190220.0'] +REQUIREMENTS = ['home-assistant-frontend==20190227.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 2354019908e..52382f1de06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190220.0 +home-assistant-frontend==20190227.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e672549c110..deed85b20ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -116,7 +116,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190220.0 +home-assistant-frontend==20190227.0 # homeassistant.components.homekit_controller homekit==0.12.2 From 78217fa9b0850d7818caa2fcb4461dbe328d6b65 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Feb 2019 16:34:13 -0800 Subject: [PATCH 210/253] Update translations --- .../ambient_station/.translations/de.json | 19 ++++++ .../ambient_station/.translations/es.json | 9 +++ .../ambient_station/.translations/he.json | 15 +++++ .../ambient_station/.translations/sv.json | 19 ++++++ .../components/auth/.translations/ru.json | 4 +- .../components/daikin/.translations/ru.json | 2 +- .../components/daikin/.translations/sv.json | 19 ++++++ .../deconz/.translations/es-419.json | 2 +- .../components/deconz/.translations/sv.json | 2 +- .../dialogflow/.translations/ru.json | 2 +- .../dialogflow/.translations/sv.json | 18 ++++++ .../components/ebusd/.translations/he.json | 6 ++ .../components/ebusd/.translations/sv.json | 6 ++ .../emulated_roku/.translations/de.json | 21 +++++++ .../emulated_roku/.translations/sv.json | 21 +++++++ .../components/esphome/.translations/ca.json | 4 ++ .../components/esphome/.translations/en.json | 6 +- .../esphome/.translations/es-419.json | 2 +- .../components/esphome/.translations/es.json | 6 +- .../components/esphome/.translations/ko.json | 8 ++- .../components/esphome/.translations/lb.json | 4 ++ .../components/esphome/.translations/ru.json | 8 ++- .../components/esphome/.translations/sv.json | 30 +++++++++ .../esphome/.translations/zh-Hant.json | 4 ++ .../components/geofency/.translations/de.json | 18 ++++++ .../components/geofency/.translations/es.json | 3 + .../components/geofency/.translations/ru.json | 2 +- .../components/geofency/.translations/sv.json | 18 ++++++ .../gpslogger/.translations/de.json | 18 ++++++ .../gpslogger/.translations/ru.json | 2 +- .../gpslogger/.translations/sv.json | 18 ++++++ .../.translations/es-419.json | 2 +- .../homematicip_cloud/.translations/sv.json | 2 +- .../components/hue/.translations/sv.json | 2 +- .../components/ifttt/.translations/ru.json | 2 +- .../components/ipma/.translations/he.json | 18 ++++++ .../components/ipma/.translations/no.json | 4 +- .../components/ipma/.translations/sv.json | 19 ++++++ .../components/locative/.translations/de.json | 18 ++++++ .../components/locative/.translations/ru.json | 2 +- .../components/locative/.translations/sv.json | 18 ++++++ .../luftdaten/.translations/sv.json | 19 ++++++ .../components/mailgun/.translations/ru.json | 2 +- .../components/mailgun/.translations/sv.json | 18 ++++++ .../components/mqtt/.translations/ru.json | 2 +- .../components/nest/.translations/ru.json | 2 +- .../owntracks/.translations/es.json | 17 +++++ .../owntracks/.translations/ru.json | 2 +- .../owntracks/.translations/sv.json | 17 +++++ .../components/point/.translations/ca.json | 2 +- .../components/point/.translations/es.json | 14 +++++ .../components/point/.translations/ru.json | 2 +- .../components/point/.translations/sv.json | 61 +++++++++--------- .../components/ps4/.translations/de.json | 32 ++++++++++ .../components/ps4/.translations/es.json | 27 ++++++++ .../components/ps4/.translations/he.json | 25 ++++++++ .../components/ps4/.translations/ko.json | 2 +- .../components/ps4/.translations/no.json | 32 ++++++++++ .../components/ps4/.translations/sv.json | 32 ++++++++++ .../components/ps4/.translations/zh-Hant.json | 2 +- .../rainmachine/.translations/es.json | 19 ++++++ .../rainmachine/.translations/sv.json | 19 ++++++ .../simplisafe/.translations/sv.json | 19 ++++++ .../smartthings/.translations/de.json | 11 ++++ .../smartthings/.translations/es.json | 23 +++++++ .../smartthings/.translations/he.json | 28 +++++++++ .../smartthings/.translations/no.json | 3 +- .../smartthings/.translations/sv.json | 28 +++++++++ .../tellduslive/.translations/de.json | 4 ++ .../tellduslive/.translations/es.json | 6 +- .../tellduslive/.translations/sv.json | 28 +++++++++ .../components/toon/.translations/ca.json | 34 ++++++++++ .../components/toon/.translations/en.json | 62 +++++++++---------- .../components/toon/.translations/es-419.json | 18 ++++++ .../components/toon/.translations/ko.json | 34 ++++++++++ .../components/toon/.translations/lb.json | 34 ++++++++++ .../components/toon/.translations/ru.json | 34 ++++++++++ .../toon/.translations/zh-Hant.json | 34 ++++++++++ .../components/tplink/.translations/ca.json | 15 +++++ .../components/tplink/.translations/da.json | 15 +++++ .../components/tplink/.translations/de.json | 15 +++++ .../tplink/.translations/es-419.json | 10 +++ .../components/tplink/.translations/es.json | 15 +++++ .../components/tplink/.translations/he.json | 15 +++++ .../components/tplink/.translations/ko.json | 15 +++++ .../components/tplink/.translations/lb.json | 15 +++++ .../components/tplink/.translations/no.json | 15 +++++ .../components/tplink/.translations/pl.json | 15 +++++ .../components/tplink/.translations/ru.json | 15 +++++ .../components/tplink/.translations/sv.json | 15 +++++ .../tplink/.translations/zh-Hant.json | 15 +++++ .../components/twilio/.translations/ru.json | 2 +- .../components/twilio/.translations/sv.json | 18 ++++++ .../components/unifi/.translations/sv.json | 26 ++++++++ .../components/upnp/.translations/es.json | 8 ++- .../components/upnp/.translations/sv.json | 9 ++- .../components/zha/.translations/de.json | 1 + .../components/zha/.translations/es.json | 21 +++++++ .../components/zha/.translations/sv.json | 21 +++++++ 99 files changed, 1350 insertions(+), 97 deletions(-) create mode 100644 homeassistant/components/ambient_station/.translations/de.json create mode 100644 homeassistant/components/ambient_station/.translations/es.json create mode 100644 homeassistant/components/ambient_station/.translations/he.json create mode 100644 homeassistant/components/ambient_station/.translations/sv.json create mode 100644 homeassistant/components/daikin/.translations/sv.json create mode 100644 homeassistant/components/dialogflow/.translations/sv.json create mode 100644 homeassistant/components/ebusd/.translations/he.json create mode 100644 homeassistant/components/ebusd/.translations/sv.json create mode 100644 homeassistant/components/emulated_roku/.translations/de.json create mode 100644 homeassistant/components/emulated_roku/.translations/sv.json create mode 100644 homeassistant/components/esphome/.translations/sv.json create mode 100644 homeassistant/components/geofency/.translations/de.json create mode 100644 homeassistant/components/geofency/.translations/sv.json create mode 100644 homeassistant/components/gpslogger/.translations/de.json create mode 100644 homeassistant/components/gpslogger/.translations/sv.json create mode 100644 homeassistant/components/ipma/.translations/he.json create mode 100644 homeassistant/components/ipma/.translations/sv.json create mode 100644 homeassistant/components/locative/.translations/de.json create mode 100644 homeassistant/components/locative/.translations/sv.json create mode 100644 homeassistant/components/luftdaten/.translations/sv.json create mode 100644 homeassistant/components/mailgun/.translations/sv.json create mode 100644 homeassistant/components/owntracks/.translations/es.json create mode 100644 homeassistant/components/owntracks/.translations/sv.json create mode 100644 homeassistant/components/point/.translations/es.json create mode 100644 homeassistant/components/ps4/.translations/de.json create mode 100644 homeassistant/components/ps4/.translations/es.json create mode 100644 homeassistant/components/ps4/.translations/he.json create mode 100644 homeassistant/components/ps4/.translations/no.json create mode 100644 homeassistant/components/ps4/.translations/sv.json create mode 100644 homeassistant/components/rainmachine/.translations/es.json create mode 100644 homeassistant/components/rainmachine/.translations/sv.json create mode 100644 homeassistant/components/simplisafe/.translations/sv.json create mode 100644 homeassistant/components/smartthings/.translations/es.json create mode 100644 homeassistant/components/smartthings/.translations/he.json create mode 100644 homeassistant/components/smartthings/.translations/sv.json create mode 100644 homeassistant/components/tellduslive/.translations/sv.json create mode 100644 homeassistant/components/toon/.translations/ca.json create mode 100644 homeassistant/components/toon/.translations/es-419.json create mode 100644 homeassistant/components/toon/.translations/ko.json create mode 100644 homeassistant/components/toon/.translations/lb.json create mode 100644 homeassistant/components/toon/.translations/ru.json create mode 100644 homeassistant/components/toon/.translations/zh-Hant.json create mode 100644 homeassistant/components/tplink/.translations/ca.json create mode 100644 homeassistant/components/tplink/.translations/da.json create mode 100644 homeassistant/components/tplink/.translations/de.json create mode 100644 homeassistant/components/tplink/.translations/es-419.json create mode 100644 homeassistant/components/tplink/.translations/es.json create mode 100644 homeassistant/components/tplink/.translations/he.json create mode 100644 homeassistant/components/tplink/.translations/ko.json create mode 100644 homeassistant/components/tplink/.translations/lb.json create mode 100644 homeassistant/components/tplink/.translations/no.json create mode 100644 homeassistant/components/tplink/.translations/pl.json create mode 100644 homeassistant/components/tplink/.translations/ru.json create mode 100644 homeassistant/components/tplink/.translations/sv.json create mode 100644 homeassistant/components/tplink/.translations/zh-Hant.json create mode 100644 homeassistant/components/twilio/.translations/sv.json create mode 100644 homeassistant/components/unifi/.translations/sv.json create mode 100644 homeassistant/components/zha/.translations/es.json create mode 100644 homeassistant/components/zha/.translations/sv.json diff --git a/homeassistant/components/ambient_station/.translations/de.json b/homeassistant/components/ambient_station/.translations/de.json new file mode 100644 index 00000000000..1431efbf167 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Anwendungsschl\u00fcssel und / oder API-Schl\u00fcssel bereits registriert", + "invalid_key": "Ung\u00fcltiger API Key und / oder Anwendungsschl\u00fcssel", + "no_devices": "Keine Ger\u00e4te im Konto gefunden" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "app_key": "Anwendungsschl\u00fcssel" + }, + "title": "Gib deine Informationen ein" + } + }, + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/es.json b/homeassistant/components/ambient_station/.translations/es.json new file mode 100644 index 00000000000..d6732423a7e --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/es.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Completa tu informaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/he.json b/homeassistant/components/ambient_station/.translations/he.json new file mode 100644 index 00000000000..f5afbca71c0 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "no_devices": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05df \u05d1\u05d7\u05e9\u05d1\u05d5\u05df" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + }, + "title": "\u05de\u05dc\u05d0 \u05d0\u05ea \u05d4\u05e4\u05e8\u05d8\u05d9\u05dd \u05e9\u05dc\u05da" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/sv.json b/homeassistant/components/ambient_station/.translations/sv.json new file mode 100644 index 00000000000..c429d439503 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Applikationsnyckel och/eller API-nyckel \u00e4r redan registrerade", + "invalid_key": "Ogiltigt API-nyckel och/eller applikationsnyckel", + "no_devices": "Inga enheter hittades i kontot" + }, + "step": { + "user": { + "data": { + "api_key": "API-nyckel", + "app_key": "Applikationsnyckel" + }, + "title": "Fyll i dina uppgifter" + } + }, + "title": "Ambient Weather PWS (Personal Weather Station)" + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/ru.json b/homeassistant/components/auth/.translations/ru.json index edf136bd7f3..5092e079250 100644 --- a/homeassistant/components/auth/.translations/ru.json +++ b/homeassistant/components/auth/.translations/ru.json @@ -21,11 +21,11 @@ }, "totp": { "error": { - "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0432\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f." + "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0412\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f." }, "step": { "init": { - "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043e\u0442\u0441\u043a\u0430\u043d\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0434\u043b\u0438\u043d\u043d\u043e\u0441\u0442\u0438. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0433\u043e \u043d\u0435\u0442, \u043c\u044b \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043b\u0438\u0431\u043e [Google Authenticator](https://support.google.com/accounts/answer/1066447), \u043b\u0438\u0431\u043e [Authy](https://authy.com/). \n\n {qr_code} \n \n\u041f\u043e\u0441\u043b\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f QR-\u043a\u043e\u0434\u0430 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0432\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c QR-\u043a\u043e\u0434\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043e\u0434\u0430 **`{code}`**.", + "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043e\u0442\u0441\u043a\u0430\u043d\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0434\u043b\u0438\u043d\u043d\u043e\u0441\u0442\u0438. \u0415\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0435\u0433\u043e \u043d\u0435\u0442, \u043c\u044b \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043b\u0438\u0431\u043e [Google Authenticator](https://support.google.com/accounts/answer/1066447), \u043b\u0438\u0431\u043e [Authy](https://authy.com/). \n\n {qr_code} \n \n\u041f\u043e\u0441\u043b\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f QR-\u043a\u043e\u0434\u0430 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0412\u0430\u0448\u0435\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u0415\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c QR-\u043a\u043e\u0434\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043e\u0434\u0430 **`{code}`**.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c TOTP" } }, diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json index 83c42f4280c..0549aa3b160 100644 --- a/homeassistant/components/daikin/.translations/ru.json +++ b/homeassistant/components/daikin/.translations/ru.json @@ -10,7 +10,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0432\u0430\u0448\u0435\u0433\u043e Daikin AC.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0412\u0430\u0448\u0435\u0433\u043e Daikin AC.", "title": "Daikin AC" } }, diff --git a/homeassistant/components/daikin/.translations/sv.json b/homeassistant/components/daikin/.translations/sv.json new file mode 100644 index 00000000000..0f1247197aa --- /dev/null +++ b/homeassistant/components/daikin/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "device_fail": "Ov\u00e4ntat fel vid skapande av enhet.", + "device_timeout": "Timeout f\u00f6r anslutning till enheten." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rddatorn" + }, + "description": "Ange IP-adressen f\u00f6r din Daikin AC.", + "title": "Konfigurera Daikin AC" + } + }, + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/es-419.json b/homeassistant/components/deconz/.translations/es-419.json index 7d8f77fe3a2..c2298a5fcc2 100644 --- a/homeassistant/components/deconz/.translations/es-419.json +++ b/homeassistant/components/deconz/.translations/es-419.json @@ -12,7 +12,7 @@ "init": { "data": { "host": "Host", - "port": "Puerto (valor predeterminado: '80')" + "port": "Puerto" }, "title": "Definir el gateway deCONZ" }, diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json index 88cf8742acd..a1157cbfb9c 100644 --- a/homeassistant/components/deconz/.translations/sv.json +++ b/homeassistant/components/deconz/.translations/sv.json @@ -28,6 +28,6 @@ "title": "Extra konfigurationsalternativ f\u00f6r deCONZ" } }, - "title": "deCONZ" + "title": "deCONZ Zigbee Gateway" } } \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/ru.json b/homeassistant/components/dialogflow/.translations/ru.json index 8625780e65c..899f776c095 100644 --- a/homeassistant/components/dialogflow/.translations/ru.json +++ b/homeassistant/components/dialogflow/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [webhooks \u0434\u043b\u044f Dialogflow]({dialogflow_url}).\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [webhooks \u0434\u043b\u044f Dialogflow]({dialogflow_url}).\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/sv.json b/homeassistant/components/dialogflow/.translations/sv.json new file mode 100644 index 00000000000..07fe5e11217 --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot Dialogflow meddelanden.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera [webhook funktionen i Dialogflow]({dialogflow_url}).\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Dialogflow?", + "title": "Konfigurera Dialogflow Webhook" + } + }, + "title": "Dialogflow" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/he.json b/homeassistant/components/ebusd/.translations/he.json new file mode 100644 index 00000000000..0232fc3044d --- /dev/null +++ b/homeassistant/components/ebusd/.translations/he.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "\u05d9\u05d5\u05dd", + "night": "\u05dc\u05d9\u05dc\u05d4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/sv.json b/homeassistant/components/ebusd/.translations/sv.json new file mode 100644 index 00000000000..92f4355066d --- /dev/null +++ b/homeassistant/components/ebusd/.translations/sv.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Dag", + "night": "Natt" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/de.json b/homeassistant/components/emulated_roku/.translations/de.json new file mode 100644 index 00000000000..f9c8a21240a --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "Name existiert bereits" + }, + "step": { + "user": { + "data": { + "advertise_ip": "IP Adresse annoncieren", + "advertise_port": "Port annoncieren", + "host_ip": "Host-IP", + "listen_port": "Listen-Port", + "name": "Name", + "upnp_bind_multicast": "Multicast binden (True/False)" + }, + "title": "Serverkonfiguration definieren" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/sv.json b/homeassistant/components/emulated_roku/.translations/sv.json new file mode 100644 index 00000000000..4ae7a356c4c --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "name_exists": "Namnet finns redan" + }, + "step": { + "user": { + "data": { + "advertise_ip": "Annonsera med IP", + "advertise_port": "Annonsera p\u00e5 port", + "host_ip": "IP p\u00e5 v\u00e4rddatorn", + "listen_port": "Lyssna p\u00e5 port", + "name": "Namn", + "upnp_bind_multicast": "Bind multicast (True/False)" + }, + "title": "Definiera serverkonfiguration" + } + }, + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/ca.json b/homeassistant/components/esphome/.translations/ca.json index 9fe0bb5e9d0..74d2c1d4b9a 100644 --- a/homeassistant/components/esphome/.translations/ca.json +++ b/homeassistant/components/esphome/.translations/ca.json @@ -16,6 +16,10 @@ "description": "Introdueix la contrasenya que has posat en la teva configuraci\u00f3.", "title": "Introdueix la contrasenya" }, + "discovery_confirm": { + "description": "Vols afegir el node `{name}` d'ESPHome a Home Assistant?", + "title": "Node d'ESPHome descobert" + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/esphome/.translations/en.json b/homeassistant/components/esphome/.translations/en.json index 53331ebc0a9..3a73e54c345 100644 --- a/homeassistant/components/esphome/.translations/en.json +++ b/homeassistant/components/esphome/.translations/en.json @@ -13,9 +13,13 @@ "data": { "password": "Password" }, - "description": "Please enter the password you set in your configuration.", + "description": "Please enter the password you set in your configuration for {name}.", "title": "Enter Password" }, + "discovery_confirm": { + "description": "Do you want to add the ESPHome node `{name}` to Home Assistant?", + "title": "Discovered ESPHome node" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/esphome/.translations/es-419.json b/homeassistant/components/esphome/.translations/es-419.json index 3557095d094..84000783435 100644 --- a/homeassistant/components/esphome/.translations/es-419.json +++ b/homeassistant/components/esphome/.translations/es-419.json @@ -13,7 +13,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Por favor ingrese la contrase\u00f1a que estableci\u00f3 en su configuraci\u00f3n.", + "description": "Por favor ingrese la contrase\u00f1a que estableci\u00f3 en su configuraci\u00f3n para {name} .", "title": "Escriba la contrase\u00f1a" }, "user": { diff --git a/homeassistant/components/esphome/.translations/es.json b/homeassistant/components/esphome/.translations/es.json index 8010b330b88..c4b18899eaf 100644 --- a/homeassistant/components/esphome/.translations/es.json +++ b/homeassistant/components/esphome/.translations/es.json @@ -18,8 +18,10 @@ "data": { "host": "Host", "port": "Puerto" - } + }, + "title": "ESPHome" } - } + }, + "title": "ESPHome" } } \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/ko.json b/homeassistant/components/esphome/.translations/ko.json index 24f84851254..f58d43f9df9 100644 --- a/homeassistant/components/esphome/.translations/ko.json +++ b/homeassistant/components/esphome/.translations/ko.json @@ -6,16 +6,20 @@ "error": { "connection_error": "ESP \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. YAML \ud30c\uc77c\uc5d0 'api:' \ub97c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "invalid_password": "\uc798\ubabb\ub41c \ube44\ubc00\ubc88\ud638", - "resolve_error": "ESP \uc758 \uc8fc\uc18c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uac00 \uacc4\uc18d \ubc1c\uc0dd\ud558\uba74 \uace0\uc815 IP \uc8fc\uc18c (https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694" + "resolve_error": "ESP \uc758 \uc8fc\uc18c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uac00 \uacc4\uc18d \ubc1c\uc0dd\ud558\uba74 \uace0\uc815 IP \uc8fc\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "step": { "authenticate": { "data": { "password": "\ube44\ubc00\ubc88\ud638" }, - "description": "ESP \uc5d0\uc11c \uc124\uc815\ud55c \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "description": "{name} \uc758 \uad6c\uc131\uc5d0 \uc124\uc815\ud55c \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", "title": "\ube44\ubc00\ubc88\ud638 \uc785\ub825" }, + "discovery_confirm": { + "description": "Home Assistant \uc5d0 ESPHome node `{name}` \uc744(\ub97c) \ucd94\uac00 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac \ub41c ESPHome node" + }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", diff --git a/homeassistant/components/esphome/.translations/lb.json b/homeassistant/components/esphome/.translations/lb.json index 13dddd0beca..a240debfaf5 100644 --- a/homeassistant/components/esphome/.translations/lb.json +++ b/homeassistant/components/esphome/.translations/lb.json @@ -16,6 +16,10 @@ "description": "Gitt d'Passwuert vun \u00e4rer Konfiguratioun an.", "title": "Passwuert aginn" }, + "discovery_confirm": { + "description": "W\u00ebllt dir den ESPHome Provider `{name}` am 'Home Assistant dob\u00e4isetzen?", + "title": "Entdeckten ESPHome Provider" + }, "user": { "data": { "host": "Apparat", diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json index fc74825e188..2b631ea219c 100644 --- a/homeassistant/components/esphome/.translations/ru.json +++ b/homeassistant/components/esphome/.translations/ru.json @@ -4,7 +4,7 @@ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", "invalid_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c!", "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, @@ -13,9 +13,13 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 {name}.", "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c" }, + "discovery_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c ESPHome `{name}`?", + "title": "ESPHome" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/esphome/.translations/sv.json b/homeassistant/components/esphome/.translations/sv.json new file mode 100644 index 00000000000..6eadcb4e18e --- /dev/null +++ b/homeassistant/components/esphome/.translations/sv.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "ESP \u00e4r redan konfigurerad" + }, + "error": { + "connection_error": "Kan inte ansluta till ESP. Se till att din YAML-fil inneh\u00e5ller en 'api:' line.", + "invalid_password": "Ogiltigt l\u00f6senord!", + "resolve_error": "Det g\u00e5r inte att hitta IP-adressen f\u00f6r ESP med DNS-namnet. Om det h\u00e4r felet kvarst\u00e5r anger du en statisk IP-adress: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "step": { + "authenticate": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Ange det l\u00f6senord du angav i din konfiguration.", + "title": "Ange l\u00f6senord" + }, + "user": { + "data": { + "host": "V\u00e4rddatorn", + "port": "Port" + }, + "description": "Ange anslutningsinst\u00e4llningarna f\u00f6r noden [ESPHome](https://esphomelib.com/).", + "title": "ESPHome" + } + }, + "title": "ESPHome" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/zh-Hant.json b/homeassistant/components/esphome/.translations/zh-Hant.json index 055100cf077..65817470860 100644 --- a/homeassistant/components/esphome/.translations/zh-Hant.json +++ b/homeassistant/components/esphome/.translations/zh-Hant.json @@ -16,6 +16,10 @@ "description": "\u8acb\u8f38\u5165\u8a2d\u5b9a\u5167\u6240\u8a2d\u5b9a\u4e4b\u5bc6\u78bc\u3002", "title": "\u8f38\u5165\u5bc6\u78bc" }, + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u5c07 ESPHome node\u300c{name}\u300d\u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u63a2\u7d22\u5230\u7684 ESPHome node" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/geofency/.translations/de.json b/homeassistant/components/geofency/.translations/de.json new file mode 100644 index 00000000000..ad4722fa9fc --- /dev/null +++ b/homeassistant/components/geofency/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Deine Home-Assistant-Instanz muss aus dem internet erreichbar sein, um Nachrichten von Geofency zu erhalten.", + "one_instance_allowed": "Es ist nur eine einzige Instanz erforderlich." + }, + "create_entry": { + "default": "Um Ereignisse an den Home Assistant zu senden, musst das Webhook Feature in Geofency konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." + }, + "step": { + "user": { + "description": "M\u00f6chtest du den Geofency Webhook wirklich einrichten?", + "title": "Richten Sie den Geofency Webhook ein" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/es.json b/homeassistant/components/geofency/.translations/es.json index cd14e21db10..a81fc927b6b 100644 --- a/homeassistant/components/geofency/.translations/es.json +++ b/homeassistant/components/geofency/.translations/es.json @@ -3,6 +3,9 @@ "abort": { "not_internet_accessible": "Tu Home Assistant debe ser accesible desde Internet para recibir mensajes de GPSLogger.", "one_instance_allowed": "Solo se necesita una instancia." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, necesitar\u00e1s configurar la funci\u00f3n de webhook en Geofency.\n\nRellene la siguiente informaci\u00f3n:\n\n- URL: ``{webhook_url}``\n- M\u00e9todo: POST\n\nVer[la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." } } } \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/ru.json b/homeassistant/components/geofency/.translations/ru.json index 2460e28393a..6c699d21ce6 100644 --- a/homeassistant/components/geofency/.translations/ru.json +++ b/homeassistant/components/geofency/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Geofency.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Geofency.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/geofency/.translations/sv.json b/homeassistant/components/geofency/.translations/sv.json new file mode 100644 index 00000000000..88c9709147f --- /dev/null +++ b/homeassistant/components/geofency/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara \u00e5tkomlig ifr\u00e5n internet f\u00f6r att ta emot meddelanden ifr\u00e5n Geofency.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i Geofency.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Geofency Webhook?", + "title": "Konfigurera Geofency Webhook" + } + }, + "title": "Geofency Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/de.json b/homeassistant/components/gpslogger/.translations/de.json new file mode 100644 index 00000000000..82c1dfa3e53 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Deine Home-Assistant-Instanz muss aus dem internet erreichbar sein, um Nachrichten von GPSLogger zu erhalten.", + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "create_entry": { + "default": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in der GPSLogger konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." + }, + "step": { + "user": { + "description": "M\u00f6chten Sie den GPSLogger Webhook wirklich einrichten?", + "title": "GPSLogger Webhook einrichten" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/ru.json b/homeassistant/components/gpslogger/.translations/ru.json index ac9c1c2d43e..366cb1735d5 100644 --- a/homeassistant/components/gpslogger/.translations/ru.json +++ b/homeassistant/components/gpslogger/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f GPSLogger.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f GPSLogger.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/gpslogger/.translations/sv.json b/homeassistant/components/gpslogger/.translations/sv.json new file mode 100644 index 00000000000..3a927a70e61 --- /dev/null +++ b/homeassistant/components/gpslogger/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara \u00e5tkomlig ifr\u00e5n internet f\u00f6r att ta emot meddelanden ifr\u00e5n GPSLogger.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i GPSLogger.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera GPSLogger Webhook?", + "title": "Konfigurera GPSLogger Webhook" + } + }, + "title": "GPSLogger Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/es-419.json b/homeassistant/components/homematicip_cloud/.translations/es-419.json index 61d7defcb66..5102b25aaee 100644 --- a/homeassistant/components/homematicip_cloud/.translations/es-419.json +++ b/homeassistant/components/homematicip_cloud/.translations/es-419.json @@ -19,7 +19,7 @@ } }, "link": { - "description": "Presione el bot\u00f3n azul en el punto de acceso y el bot\u00f3n enviar para registrar HomematicIP con Home Assistant. \n\n ! [Ubicaci\u00f3n del bot\u00f3n en el puente] (/ static / images / config_flows / config_homematicip_cloud.png)" + "description": "Presione el bot\u00f3n azul en el punto de acceso y el bot\u00f3n enviar para registrar HomematicIP con Home Assistant. \n\n ! [Ubicaci\u00f3n del bot\u00f3n en el puente] (/static/images/config_flows/config_homematicip_cloud.png)" } }, "title": "HomematicIP Cloud" diff --git a/homeassistant/components/homematicip_cloud/.translations/sv.json b/homeassistant/components/homematicip_cloud/.translations/sv.json index da6bde77ae3..f155e8fd1c1 100644 --- a/homeassistant/components/homematicip_cloud/.translations/sv.json +++ b/homeassistant/components/homematicip_cloud/.translations/sv.json @@ -21,7 +21,7 @@ "title": "V\u00e4lj HomematicIP Accesspunkt" }, "link": { - "description": "Tryck p\u00e5 den bl\u00e5 knappen p\u00e5 accesspunkten och p\u00e5 skickaknappen f\u00f6r att registrera HomematicIP med Home-Assistant. \n\n ![Placering av knapp p\u00e5 bryggan](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Tryck p\u00e5 den bl\u00e5 knappen p\u00e5 accesspunkten och p\u00e5 skicka-knappen f\u00f6r att registrera HomematicIP med Home Assistant. \n\n ![Placering av knappen p\u00e5 bryggan](/static/images/config_flows/config_homematicip_cloud.png)", "title": "L\u00e4nka Accesspunkt" } }, diff --git a/homeassistant/components/hue/.translations/sv.json b/homeassistant/components/hue/.translations/sv.json index efbcfa544f5..a7ffc7bacb2 100644 --- a/homeassistant/components/hue/.translations/sv.json +++ b/homeassistant/components/hue/.translations/sv.json @@ -24,6 +24,6 @@ "title": "L\u00e4nka hub" } }, - "title": "Philips Hue Brygga" + "title": "Philips Hue Bridge" } } \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ru.json b/homeassistant/components/ifttt/.translations/ru.json index dc846993e2e..4184d2dfadc 100644 --- a/homeassistant/components/ifttt/.translations/ru.json +++ b/homeassistant/components/ifttt/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \"Make a web request\" \u0438\u0437 [IFTTT Webhook applet]({applet_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \"Make a web request\" \u0438\u0437 [IFTTT Webhook applet]({applet_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." }, "step": { "user": { diff --git a/homeassistant/components/ipma/.translations/he.json b/homeassistant/components/ipma/.translations/he.json new file mode 100644 index 00000000000..4931fcaf94c --- /dev/null +++ b/homeassistant/components/ipma/.translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "name_exists": "\u05d4\u05e9\u05dd \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05dd" + }, + "step": { + "user": { + "data": { + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "name": "\u05e9\u05dd" + }, + "title": "\u05de\u05d9\u05e7\u05d5\u05dd" + } + }, + "title": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05de\u05d6\u05d2 \u05d0\u05d5\u05d5\u05d9\u05e8 \u05e4\u05d5\u05e8\u05d8\u05d5\u05d2\u05d6\u05d9\u05ea (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/no.json b/homeassistant/components/ipma/.translations/no.json index a24ec4904cb..1d5aa9c40cf 100644 --- a/homeassistant/components/ipma/.translations/no.json +++ b/homeassistant/components/ipma/.translations/no.json @@ -10,8 +10,10 @@ "longitude": "Lengdegrad", "name": "Navn" }, + "description": "Instituto Portugu\u00eas do Mar e Atmosfera", "title": "Plassering" } - } + }, + "title": "Portugisisk v\u00e6rtjeneste (IPMA)" } } \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/sv.json b/homeassistant/components/ipma/.translations/sv.json new file mode 100644 index 00000000000..4bdba6f0d08 --- /dev/null +++ b/homeassistant/components/ipma/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Namnet finns redan" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" + }, + "description": "Portugisiska institutet f\u00f6r hav och atmosf\u00e4ren", + "title": "Location" + } + }, + "title": "Portugisiska weather service (IPMA)" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/de.json b/homeassistant/components/locative/.translations/de.json new file mode 100644 index 00000000000..14e0523fcf6 --- /dev/null +++ b/homeassistant/components/locative/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Deine Home-Assistant-Instanz muss aus dem internet erreichbar sein, um Nachrichten von Geofency zu erhalten.", + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "create_entry": { + "default": "Um Standorte Home Assistant zu senden, muss das Webhook Feature in der Locative App konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." + }, + "step": { + "user": { + "description": "M\u00f6chten Sie den Locative Webhook wirklich einrichten?", + "title": "Locative Webhook einrichten" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/ru.json b/homeassistant/components/locative/.translations/ru.json index ff07393da04..70f08595f3a 100644 --- a/homeassistant/components/locative/.translations/ru.json +++ b/homeassistant/components/locative/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Locative.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Locative.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/locative/.translations/sv.json b/homeassistant/components/locative/.translations/sv.json new file mode 100644 index 00000000000..0296d079938 --- /dev/null +++ b/homeassistant/components/locative/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara \u00e5tkomlig ifr\u00e5n internet f\u00f6r att ta emot meddelanden ifr\u00e5n Geofency.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i Locative appen.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Locative Webhook?", + "title": "Konfigurera Locative Webhook" + } + }, + "title": "Locative Webhook" + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/.translations/sv.json b/homeassistant/components/luftdaten/.translations/sv.json new file mode 100644 index 00000000000..01fd9ec721b --- /dev/null +++ b/homeassistant/components/luftdaten/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "communication_error": "Det g\u00e5r inte att kommunicera med Luftdaten API", + "invalid_sensor": "Sensor saknas eller \u00e4r ogiltig", + "sensor_exists": "Sensorn \u00e4r redan registrerad" + }, + "step": { + "user": { + "data": { + "show_on_map": "Visa p\u00e5 karta", + "station_id": "Luftdaten Sensor-ID" + }, + "title": "Definiera Luftdaten" + } + }, + "title": "Luftdaten" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ru.json b/homeassistant/components/mailgun/.translations/ru.json index b1828ee28ef..39503154b6c 100644 --- a/homeassistant/components/mailgun/.translations/ru.json +++ b/homeassistant/components/mailgun/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/.translations/sv.json b/homeassistant/components/mailgun/.translations/sv.json new file mode 100644 index 00000000000..f26234e84cf --- /dev/null +++ b/homeassistant/components/mailgun/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot Mailgun meddelanden.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera [Webhooks med Mailgun]({mailgun_url}).\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n Se [dokumentationen]({docs_url}) om hur du konfigurerar automatiseringar f\u00f6r att hantera inkommande data." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Mailgun?", + "title": "Konfigurera Mailgun Webhook" + } + }, + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json index 663d79f3c14..ad3a90383b1 100644 --- a/homeassistant/components/mqtt/.translations/ru.json +++ b/homeassistant/components/mqtt/.translations/ru.json @@ -15,7 +15,7 @@ "port": "\u041f\u043e\u0440\u0442", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0432\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.", "title": "MQTT" }, "hassio_confirm": { diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json index 54ff1dff999..ff86c34ac71 100644 --- a/homeassistant/components/nest/.translations/ru.json +++ b/homeassistant/components/nest/.translations/ru.json @@ -17,7 +17,7 @@ "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.", + "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 \u0412\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": { diff --git a/homeassistant/components/owntracks/.translations/es.json b/homeassistant/components/owntracks/.translations/es.json new file mode 100644 index 00000000000..f866aa6e403 --- /dev/null +++ b/homeassistant/components/owntracks/.translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Solo se necesita una instancia." + }, + "create_entry": { + "default": "\n\nEn Android, abra[la aplicaci\u00f3n OwnTracks]({android_url}), vaya a Preferencias -> Conexi\u00f3n. Cambie los siguientes ajustes:\n - Mode: HTTP privado\n - URL: {webhook_url}\n - Identificaci\u00f3n:\n - Nombre de usuario: \n - ID de dispositivo: \n\nEn iOS, abra[la aplicaci\u00f3n OwnTracks] ({ios_url}), toque el icono (i) en la parte superior izquierda -> configuraci\u00f3n. Cambie los siguientes ajustes:\n - Mode: HTTP\n - URL: {webhook_url}\n - Activar la autenticaci\u00f3n\n - UserID: \n\n{secret}\n\nConsulte[la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s informaci\u00f3n." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1s seguro de que quieres configurar OwnTracks?", + "title": "Configurar OwnTracks" + } + }, + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/ru.json b/homeassistant/components/owntracks/.translations/ru.json index bb9c7f39c5b..6ebaa31cacf 100644 --- a/homeassistant/components/owntracks/.translations/ru.json +++ b/homeassistant/components/owntracks/.translations/ru.json @@ -4,7 +4,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0415\u0441\u043b\u0438 \u0432\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0432\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u0435\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u0435\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/.translations/sv.json b/homeassistant/components/owntracks/.translations/sv.json new file mode 100644 index 00000000000..2077cceeb4d --- /dev/null +++ b/homeassistant/components/owntracks/.translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "\n\n P\u00e5 Android, \u00f6ppna [OwnTracks-appen]({android_url}), g\u00e5 till inst\u00e4llningar -> anslutning. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: Privat HTTP \n - V\u00e4rden: {webhook_url}\n - Identifiering: \n - Anv\u00e4ndarnamn: ``\n - Enhets-ID: `` \n\n P\u00e5 IOS, \u00f6ppna [OwnTracks-appen]({ios_url}), tryck p\u00e5 (i) ikonen i \u00f6vre v\u00e4nstra h\u00f6rnet -> inst\u00e4llningarna. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: HTTP \n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autentisering \n - UserID: `` \n\n {secret} \n \n Se [dokumentationen]({docs_url}) f\u00f6r mer information." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera OwnTracks?", + "title": "Konfigurera OwnTracks" + } + }, + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/ca.json b/homeassistant/components/point/.translations/ca.json index 6a66735e6d0..b50a1169a53 100644 --- a/homeassistant/components/point/.translations/ca.json +++ b/homeassistant/components/point/.translations/ca.json @@ -5,7 +5,7 @@ "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "S'ha acabat el temps d'espera mentre \u00e9s generava l'URL d'autoritzaci\u00f3.", "external_setup": "Point s'ha configurat correctament des d'un altre lloc.", - "no_flows": "Necessites configurar Point abans de poder autenticar-t'hi. [Llegiu les instruccions](https://www.home-assistant.io/components/point/)." + "no_flows": "Necessites configurar Point abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/point/)." }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Minut per als teus dispositiu/s Point." diff --git a/homeassistant/components/point/.translations/es.json b/homeassistant/components/point/.translations/es.json new file mode 100644 index 00000000000..815f8fbf9af --- /dev/null +++ b/homeassistant/components/point/.translations/es.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_setup": "S\u00f3lo se puede configurar una cuenta de Point." + }, + "step": { + "user": { + "data": { + "flow_impl": "Proveedor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/ru.json b/homeassistant/components/point/.translations/ru.json index 7bcf275f96e..60c1d62ab91 100644 --- a/homeassistant/components/point/.translations/ru.json +++ b/homeassistant/components/point/.translations/ru.json @@ -23,7 +23,7 @@ "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 Point.", + "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 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0432\u0445\u043e\u0434 \u0432 Point.", "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } }, diff --git a/homeassistant/components/point/.translations/sv.json b/homeassistant/components/point/.translations/sv.json index 6464434eda4..c68fd29f7fc 100644 --- a/homeassistant/components/point/.translations/sv.json +++ b/homeassistant/components/point/.translations/sv.json @@ -1,33 +1,32 @@ { - "config": { - "title": "Minut Point", - "step": { - "user": { - "title": "Autentiseringsleverant\u00f6r", - "description": "V\u00e4lj den autentiseringsleverant\u00f6r som du vill autentisera med mot Point.", - "data": { - "flow_impl": "Leverant\u00f6r" - } - }, - "auth": { - "title": "Autentisera Point", - "description": "F\u00f6lj l\u00e4nken nedan och klicka p\u00e5 Accept f\u00f6r att tilll\u00e5ta tillg\u00e5ng till ditt Minut konto, kom d\u00f6refter tillbaka hit och kicka p\u00e5 Submit nedan.\n\n[L\u00e4nk]({authorization_url})" - } - }, - "create_entry": { - "default": "Autentiserad med Minut f\u00f6r era Point enheter." - }, - "error": { - "no_token": "Inte autentiserad hos Minut", - "follow_link": "F\u00f6lj l\u00e4nken och autentisera innan du kickar på Submit" - }, - "abort": { - "already_setup": "Du kan endast konfigurera ett Point-konto.", - "external_setup": "Point har lyckats konfigureras fr\u00e5n ett annat fl\u00f6de.", - "no_flows": "Du m\u00e5ste konfigurera Nest innan du kan autentisera med det. [V\u00e4nligen l\u00e4s instruktionerna] (https://www.home-assistant.io/components/point/).", - "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress.", - "authorize_url_fail": "Ok\u00e4nt fel vid generering av autentisieringsadress." + "config": { + "abort": { + "already_setup": "Du kan endast konfigurera ett Point-konto.", + "authorize_url_fail": "Ok\u00e4nt fel n\u00e4r f\u00f6rs\u00f6ker generera en url f\u00f6r auktorisering.", + "authorize_url_timeout": "Timeout n\u00e4r genererar url f\u00f6r auktorisering.", + "external_setup": "Point har lyckats med konfigurering ifr\u00e5n ett annat fl\u00f6de.", + "no_flows": "Du beh\u00f6ver konfigurera Point innan de kan autentisera med den. [L\u00e4s instruktioner](https://www.home-assistant.io/components/point/)." + }, + "create_entry": { + "default": "Lyckad autentisering med Minut f\u00f6r din(a) Point-enhet(er)" + }, + "error": { + "follow_link": "F\u00f6lj l\u00e4nken och autentisera innan du trycker p\u00e5 Skicka", + "no_token": "Ej autentiserad med Minut" + }, + "step": { + "auth": { + "description": "V\u00e4nligen f\u00f6lj l\u00e4nken nedan och Acceptera tillg\u00e5ng till ditt Minut-konto, kom tillbaka och tryck p\u00e5 Skicka nedan. \n\n [L\u00e4nk]({authorization_url})", + "title": "Autentisera Point" + }, + "user": { + "data": { + "flow_impl": "Leverant\u00f6r" + }, + "description": "V\u00e4lj via vilken autentiseringsleverant\u00f6r du vill autentisera med Point.", + "title": "Autentiseringsleverant\u00f6r" + } + }, + "title": "Minut Point" } - } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/de.json b/homeassistant/components/ps4/.translations/de.json new file mode 100644 index 00000000000..8f4e8838673 --- /dev/null +++ b/homeassistant/components/ps4/.translations/de.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Fehler beim Abrufen der Anmeldeinformationen.", + "devices_configured": "Alle gefundenen Ger\u00e4te sind bereits konfiguriert.", + "no_devices_found": "Es wurden keine PlayStation 4 im Netzwerk gefunden.", + "port_987_bind_error": "Bind to Port 987 nicht m\u00f6glich.", + "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich." + }, + "error": { + "login_failed": "Fehler beim Koppeln mit PlayStation 4. \u00dcberpr\u00fcfe, ob die PIN korrekt ist.", + "not_ready": "PlayStation 4 ist nicht eingeschaltet oder mit dem Netzwerk verbunden." + }, + "step": { + "creds": { + "description": "Anmeldeinformationen ben\u00f6tigt. Klicke auf \"Senden\" und dann in der PS4 Second Screen app, aktualisiere die Ger\u00e4te und w\u00e4hle das \"Home-Assistant\"-Ger\u00e4t aus, um fortzufahren.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP-Adresse", + "name": "Name", + "region": "Region" + }, + "description": "Geben Sie Ihre PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/es.json b/homeassistant/components/ps4/.translations/es.json new file mode 100644 index 00000000000..41cbd28492a --- /dev/null +++ b/homeassistant/components/ps4/.translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "credential_error": "Error al obtener las credenciales.", + "devices_configured": "Todos los dispositivos encontrados ya est\u00e1n configurados.", + "no_devices_found": "No se encuentran dispositivos PlayStation 4 en la red." + }, + "error": { + "not_ready": "PlayStation 4 no est\u00e1 encendido o conectado a la red." + }, + "step": { + "creds": { + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "Direcci\u00f3n IP", + "name": "Nombre", + "region": "Regi\u00f3n" + }, + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/he.json b/homeassistant/components/ps4/.translations/he.json new file mode 100644 index 00000000000..d9fa42b9e47 --- /dev/null +++ b/homeassistant/components/ps4/.translations/he.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "devices_configured": "\u05db\u05dc \u05d4\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05e9\u05e0\u05de\u05e6\u05d0\u05d5 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8\u05d9\u05dd.", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9 \u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4 \u05d1\u05e8\u05e9\u05ea." + }, + "error": { + "not_ready": "PlayStation 4 \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc \u05d0\u05d5 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e8\u05e9\u05ea." + }, + "step": { + "creds": { + "title": "\u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4" + }, + "link": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4 - IP", + "name": "\u05e9\u05dd", + "region": "\u05d0\u05d9\u05d6\u05d5\u05e8" + }, + "title": "\u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4" + } + }, + "title": "\u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/ko.json b/homeassistant/components/ps4/.translations/ko.json index 019fbef1ef3..ca77537e4e1 100644 --- a/homeassistant/components/ps4/.translations/ko.json +++ b/homeassistant/components/ps4/.translations/ko.json @@ -23,7 +23,7 @@ "name": "\uc774\ub984", "region": "\uc9c0\uc5ed" }, - "description": "PlayStation 4 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. 'PIN' \uc744 \ud655\uc778\ud558\ub824\uba74, PlayStation 4 \ucf58\uc194\uc5d0\uc11c '\uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud55c \ub4a4 '\ubaa8\ubc14\uc77c \uc571 \uc5f0\uacb0 \uc124\uc815'\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec '\uae30\uae30 \ucd94\uac00'\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ud45c\uc2dc\ub41c PIN\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "description": "PlayStation 4 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. 'PIN' \uc744 \ud655\uc778\ud558\ub824\uba74, PlayStation 4 \ucf58\uc194\uc5d0\uc11c '\uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud55c \ub4a4 '\ubaa8\ubc14\uc77c \uc571 \uc811\uc18d \uc124\uc815' \uc73c\ub85c \uc774\ub3d9\ud558\uc5ec '\uae30\uae30 \ub4f1\ub85d\ud558\uae30' \ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ud654\uba74\uc5d0 \ud45c\uc2dc\ub41c 8\uc790\ub9ac \uc22b\uc790\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", "title": "PlayStation 4" } }, diff --git a/homeassistant/components/ps4/.translations/no.json b/homeassistant/components/ps4/.translations/no.json new file mode 100644 index 00000000000..32687882da2 --- /dev/null +++ b/homeassistant/components/ps4/.translations/no.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Feil ved henting av legitimasjon.", + "devices_configured": "Alle enheter som ble funnet er allerede konfigurert.", + "no_devices_found": "Ingen PlayStation 4 enheter funnet p\u00e5 nettverket.", + "port_987_bind_error": "Kunne ikke binde til port 987.", + "port_997_bind_error": "Kunne ikke binde til port 997." + }, + "error": { + "login_failed": "Klarte ikke \u00e5 koble til PlayStation 4. Bekreft at PIN koden er riktig.", + "not_ready": "PlayStation 4 er ikke p\u00e5sl\u00e5tt eller koblet til nettverk." + }, + "step": { + "creds": { + "description": "Legitimasjon n\u00f8dvendig. Trykk \"Send\" og deretter i PS4-ens andre skjerm app, kan du oppdatere enheter, og velg \"Home-Assistent' enheten for \u00e5 fortsette.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN", + "ip_address": "IP adresse", + "name": "Navn", + "region": "Region" + }, + "description": "Skriv inn PlayStation 4 informasjonen din. For 'PIN', naviger til 'Innstillinger' p\u00e5 PlayStation 4 konsollen, deretter navigerer du til 'Innstillinger for mobilapp forbindelse' og velger 'Legg til enhet'. Skriv inn PIN-koden som vises.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/sv.json b/homeassistant/components/ps4/.translations/sv.json new file mode 100644 index 00000000000..d35efbd4b00 --- /dev/null +++ b/homeassistant/components/ps4/.translations/sv.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "credential_error": "Fel n\u00e4r f\u00f6rs\u00f6ker h\u00e4mta autentiseringsuppgifter.", + "devices_configured": "Alla enheter som hittats \u00e4r redan konfigurerade.", + "no_devices_found": "Inga PlayStation 4 enheter hittades p\u00e5 n\u00e4tverket.", + "port_987_bind_error": "Kunde inte binda till port 987.", + "port_997_bind_error": "Kunde inte binda till port 997." + }, + "error": { + "login_failed": "Misslyckades med att para till PlayStation 4. Verifiera PIN-koden \u00e4r korrekt.", + "not_ready": "PlayStation 4 \u00e4r inte p\u00e5slagen eller ansluten till n\u00e4tverket." + }, + "step": { + "creds": { + "description": "Autentiseringsuppgifter beh\u00f6vs. Tryck p\u00e5 'Skicka' och sedan uppdatera enheter i appen \"PS4 Second Screen\" p\u00e5 din mobiltelefon eller surfplatta och v\u00e4lj 'Home Assistent' enheten att forts\u00e4tta.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN-kod", + "ip_address": "IP-adress", + "name": "Namn", + "region": "Region" + }, + "description": "Ange din PlayStation 4 information. F\u00f6r 'PIN', navigera till 'Inst\u00e4llningar' p\u00e5 din PlayStation 4 konsol. Navigera sedan till \"Inst\u00e4llningar f\u00f6r mobilappanslutning\" och v\u00e4lj \"L\u00e4gg till enhet\". Ange PIN-koden som visas.", + "title": "PlayStation 4" + } + }, + "title": "PlayStation 4" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/zh-Hant.json b/homeassistant/components/ps4/.translations/zh-Hant.json index b8083eba4ac..b4f45986c1e 100644 --- a/homeassistant/components/ps4/.translations/zh-Hant.json +++ b/homeassistant/components/ps4/.translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "credential_error": "\u53d6\u5f97\u6191\u8b49\u932f\u8aa4\u3002", "devices_configured": "\u6240\u6709\u88dd\u7f6e\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 PlayStation 4 \u8a2d\u5099\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 PlayStation 4 \u88dd\u7f6e\u3002", "port_987_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 987\u3002", "port_997_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 997\u3002" }, diff --git a/homeassistant/components/rainmachine/.translations/es.json b/homeassistant/components/rainmachine/.translations/es.json new file mode 100644 index 00000000000..2cb49dc0ac1 --- /dev/null +++ b/homeassistant/components/rainmachine/.translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Cuenta ya registrada", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "step": { + "user": { + "data": { + "ip_address": "Nombre de host o direcci\u00f3n IP", + "password": "Contrase\u00f1a", + "port": "Puerto" + }, + "title": "Completa tu informaci\u00f3n" + } + }, + "title": "RainMachine" + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/sv.json b/homeassistant/components/rainmachine/.translations/sv.json new file mode 100644 index 00000000000..03f9c671c35 --- /dev/null +++ b/homeassistant/components/rainmachine/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Kontot \u00e4r redan registrerat", + "invalid_credentials": "Ogiltiga autentiseringsuppgifter" + }, + "step": { + "user": { + "data": { + "ip_address": "V\u00e4rdnamn eller IP-adress", + "password": "L\u00f6senord", + "port": "Port" + }, + "title": "Fyll i dina uppgifter" + } + }, + "title": "RainMachine" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/sv.json b/homeassistant/components/simplisafe/.translations/sv.json new file mode 100644 index 00000000000..4666a9ea182 --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Kontot \u00e4r redan registrerat", + "invalid_credentials": "Ogiltiga autentiseringsuppgifter" + }, + "step": { + "user": { + "data": { + "code": "Kod (f\u00f6r Home Assistant)", + "password": "L\u00f6senord", + "username": "E-postadress" + }, + "title": "Fyll i din information" + } + }, + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/de.json b/homeassistant/components/smartthings/.translations/de.json index f65c338bf03..dd672dee9f6 100644 --- a/homeassistant/components/smartthings/.translations/de.json +++ b/homeassistant/components/smartthings/.translations/de.json @@ -1,5 +1,15 @@ { "config": { + "error": { + "app_not_installed": "Stelle sicher, dass du die Home Assistant SmartApp installiert und autorisiert hast, und versuche es erneut.", + "app_setup_error": "SmartApp kann nicht eingerichtet werden. Bitte versuche es erneut.", + "base_url_not_https": "Die `base_url` f\u00fcr die` http`-Komponente muss konfiguriert sein und mit `https://` beginnen.", + "token_already_setup": "Das Token wurde bereits eingerichtet.", + "token_forbidden": "Das Token verf\u00fcgt nicht \u00fcber die erforderlichen OAuth-Bereiche.", + "token_invalid_format": "Das Token muss im UID/GUID-Format vorliegen.", + "token_unauthorized": "Das Token ist ung\u00fcltig oder nicht mehr autorisiert.", + "webhook_error": "SmartThings konnte den in 'base_url' angegebenen Endpunkt nicht validieren. Bitte \u00fcberpr\u00fcfe die Komponentenanforderungen." + }, "step": { "user": { "data": { @@ -9,6 +19,7 @@ "title": "Gib den pers\u00f6nlichen Zugangstoken an" }, "wait_install": { + "description": "Installieren Sie Home-Assistent SmartApp an mindestens einer Stelle, und klicken Sie auf Absenden.", "title": "SmartApp installieren" } }, diff --git a/homeassistant/components/smartthings/.translations/es.json b/homeassistant/components/smartthings/.translations/es.json new file mode 100644 index 00000000000..4edeb153921 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "app_not_installed": "Por favor aseg\u00farese de haber instalado y autorizado Home Assistant SmartApp y vuelva a intentarlo.", + "app_setup_error": "No se pudo configurar el SmartApp. Por favor, int\u00e9ntelo de nuevo.", + "token_already_setup": "El token ya ha sido configurado.", + "token_invalid_format": "El token debe estar en formato UID/GUID", + "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado." + }, + "step": { + "user": { + "data": { + "access_token": "Token de acceso" + }, + "title": "Ingresar token de acceso personal" + }, + "wait_install": { + "title": "Instalar SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/he.json b/homeassistant/components/smartthings/.translations/he.json new file mode 100644 index 00000000000..c38afd989d2 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "error": { + "app_not_installed": "\u05d0\u05e0\u05d0 \u05d5\u05d3\u05d0 \u05e9\u05d4\u05ea\u05e7\u05e0\u05ea \u05d0\u05d9\u05e9\u05e8\u05ea \u05d0\u05ea Home Assistant SmartApp \u05d5\u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", + "app_setup_error": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea SmartApp. \u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "base_url_not_https": "\u05d9\u05e9 \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05d4- `base_url` \u05e2\u05d1\u05d5\u05e8 \u05e8\u05db\u05d9\u05d1` http` \u05d5\u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1- `https: //.", + "token_already_setup": "\u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05db\u05d1\u05e8 \u05d4\u05d5\u05d2\u05d3\u05e8.", + "token_forbidden": "\u05dc\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d0\u05d9\u05df \u05d0\u05ea \u05d8\u05d5\u05d5\u05d7\u05d9 OAuth \u05d4\u05d3\u05e8\u05d5\u05e9\u05d9\u05dd.", + "token_invalid_format": "\u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d7\u05d9\u05d9\u05d1 \u05dc\u05d4\u05d9\u05d5\u05ea \u05d1\u05e4\u05d5\u05e8\u05de\u05d8 UID / GUID", + "token_unauthorized": "\u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d0\u05d9\u05e0\u05d5 \u05d7\u05d5\u05e7\u05d9 \u05d0\u05d5 \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e8\u05e9\u05d4 \u05e2\u05d5\u05d3.", + "webhook_error": "SmartThings \u05dc\u05d0 \u05d4\u05e6\u05dc\u05d9\u05d7 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05e0\u05e7\u05d5\u05d3\u05ea \u05d4\u05e7\u05e6\u05d4 \u05e9\u05d4\u05d5\u05d2\u05d3\u05e8\u05d4 \u05d1- `base_url`. \u05e2\u05d9\u05d9\u05df \u05d1\u05d3\u05e8\u05d9\u05e9\u05d5\u05ea \u05d4\u05e8\u05db\u05d9\u05d1." + }, + "step": { + "user": { + "data": { + "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" + }, + "description": "\u05d4\u05d6\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea] ( {token_url} ) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea] ( {component_url} ).", + "title": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9 " + }, + "wait_install": { + "description": "\u05d4\u05ea\u05e7\u05df \u05d0\u05ea \u05d4- Home Assistant SmartApp \u05dc\u05e4\u05d7\u05d5\u05ea \u05d1\u05de\u05d9\u05e7\u05d5\u05dd \u05d0\u05d7\u05d3 \u05d5\u05dc\u05d7\u05e5 \u05e2\u05dc \u05e9\u05dc\u05d7.", + "title": "\u05d4\u05ea\u05e7\u05df \u05d0\u05ea SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/no.json b/homeassistant/components/smartthings/.translations/no.json index 4d7df8bd65d..fe93407b429 100644 --- a/homeassistant/components/smartthings/.translations/no.json +++ b/homeassistant/components/smartthings/.translations/no.json @@ -7,7 +7,8 @@ "token_already_setup": "Token har allerede blitt satt opp.", "token_forbidden": "Tollet har ikke de n\u00f8dvendige OAuth m\u00e5lene.", "token_invalid_format": "Token m\u00e5 v\u00e6re i UID/GUID format", - "token_unauthorized": "Tollet er ugyldig eller ikke lenger autorisert." + "token_unauthorized": "Tollet er ugyldig eller ikke lenger autorisert.", + "webhook_error": "SmartThings kunne ikke validere endepunktet konfigurert i `base_url`. Vennligst se komponent krav." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/sv.json b/homeassistant/components/smartthings/.translations/sv.json new file mode 100644 index 00000000000..6da4624fa39 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/sv.json @@ -0,0 +1,28 @@ +{ + "config": { + "error": { + "app_not_installed": "V\u00e4nligen se till att du har installerat och auktoriserad Home Assistant SmartApp och f\u00f6rs\u00f6k igen.", + "app_setup_error": "Det gick inte att installera Home Assistant SmartApp. V\u00e4nligen f\u00f6rs\u00f6k igen.", + "base_url_not_https": "Den `base_url`f\u00f6r `http` komponenten m\u00e5ste konfigureras och b\u00f6rja med `https://`.", + "token_already_setup": "Token har redan installerats.", + "token_forbidden": "Token har inte det som kr\u00e4vs inom omf\u00e5ng f\u00f6r OAuth.", + "token_invalid_format": "Token m\u00e5ste vara i UID/GUID-format", + "token_unauthorized": "Denna token \u00e4r ogiltig eller inte l\u00e4ngre auktoriserad.", + "webhook_error": "SmartThings kunde inte validera endpoint konfigurerad i \" base_url`. V\u00e4nligen granska kraven f\u00f6r komponenten." + }, + "step": { + "user": { + "data": { + "access_token": "\u00c5tkomsttoken" + }, + "description": "V\u00e4nligen ange en [personlig \u00e5tkomsttoken]({token_url}) f\u00f6r SmartThings som har skapats enligt [instruktionerna]({component_url}).", + "title": "Ange personlig \u00e5tkomsttoken" + }, + "wait_install": { + "description": "Installera Home Assistant SmartApp p\u00e5 minst en plats och klicka p\u00e5 Skicka.", + "title": "Installera SmartApp" + } + }, + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/de.json b/homeassistant/components/tellduslive/.translations/de.json index 4e9c32a1ee5..a9f91f16b11 100644 --- a/homeassistant/components/tellduslive/.translations/de.json +++ b/homeassistant/components/tellduslive/.translations/de.json @@ -2,10 +2,14 @@ "config": { "abort": { "all_configured": "TelldusLive ist bereits konfiguriert", + "already_setup": "TelldusLive ist bereits konfiguriert", "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "unknown": "Unbekannter Fehler ist aufgetreten" }, + "error": { + "auth_error": "Authentifizierungsfehler, bitte versuchen Sie es erneut" + }, "step": { "auth": { "description": "So verkn\u00fcpfen Sie Ihr TelldusLive-Konto: \n 1. Klicken Sie auf den Link unten \n 2. Melden Sie sich bei Telldus Live an \n 3. Autorisieren Sie ** {app_name} ** (klicken Sie auf ** Yes **). \n 4. Kommen Sie hierher zur\u00fcck und klicken Sie auf ** SUBMIT **. \n\n [Link TelldusLive-Konto]({auth_url})", diff --git a/homeassistant/components/tellduslive/.translations/es.json b/homeassistant/components/tellduslive/.translations/es.json index 4e7de72edc4..bf1aedab17d 100644 --- a/homeassistant/components/tellduslive/.translations/es.json +++ b/homeassistant/components/tellduslive/.translations/es.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_setup": "TelldusLive ya est\u00e1 configurado" + "all_configured": "TelldusLive ya est\u00e1 configurado", + "already_setup": "TelldusLive ya est\u00e1 configurado", + "authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n", + "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", + "unknown": "Se produjo un error desconocido" }, "error": { "auth_error": "Error de autenticaci\u00f3n, por favor int\u00e9ntalo de nuevo" diff --git a/homeassistant/components/tellduslive/.translations/sv.json b/homeassistant/components/tellduslive/.translations/sv.json new file mode 100644 index 00000000000..5636e137948 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/sv.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "all_configured": "Telldus Live! \u00e4r redan konfigurerad", + "already_setup": "Telldus Live! \u00e4r redan konfigurerad", + "authorize_url_fail": "Ok\u00e4nt fel n\u00e4r genererar en url f\u00f6r att auktorisera.", + "authorize_url_timeout": "Timeout n\u00e4r genererar auktorisera url.", + "unknown": "Ok\u00e4nt fel intr\u00e4ffade" + }, + "error": { + "auth_error": "Autentiseringsfel, v\u00e4nligen f\u00f6rs\u00f6k igen" + }, + "step": { + "auth": { + "description": "F\u00f6r att l\u00e4nka ditt \"Telldus Live!\" konto: \n 1. Klicka p\u00e5 l\u00e4nken nedan \n 2. Logga in p\u00e5 Telldus Live!\n 3. Godk\u00e4nn **{app_name}** (klicka **Yes**). \n 4. Kom tillbaka hit och klicka p\u00e5 **SUBMIT**. \n\n [L\u00e4nk till Telldus Live konto]({auth_url})", + "title": "Autentisera mot Telldus Live!" + }, + "user": { + "data": { + "host": "V\u00e4rddatorn" + }, + "description": "?", + "title": "V\u00e4lj endpoint." + } + }, + "title": "Telldus Live!" + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/ca.json b/homeassistant/components/toon/.translations/ca.json new file mode 100644 index 00000000000..0a88b82f829 --- /dev/null +++ b/homeassistant/components/toon/.translations/ca.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "client_id": "L'identificador de client de la configuraci\u00f3 no \u00e9s v\u00e0lid.", + "client_secret": "El codi secret de client de la configuraci\u00f3 no \u00e9s v\u00e0lid.", + "no_agreements": "Aquest compte no t\u00e9 pantalles Toon.", + "no_app": "Has de configurar Toon abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/toon/).", + "unknown_auth_fail": "S'ha produ\u00eft un error inesperat durant l'autenticaci\u00f3." + }, + "error": { + "credentials": "Les credencials proporcionades no s\u00f3n v\u00e0lides.", + "display_exists": "La pantalla seleccionada ja est\u00e0 configurada." + }, + "step": { + "authenticate": { + "data": { + "password": "Contrasenya", + "tenant": "Tenant", + "username": "Nom d'usuari" + }, + "description": "Autentica't amb el teu compte d'Eneco Toon (no el compte de desenvolupador).", + "title": "Enlla\u00e7ar compte de Toon" + }, + "display": { + "data": { + "display": "Tria la visualitzaci\u00f3" + }, + "description": "Selecciona la pantalla Toon amb la qual vols connectar-te.", + "title": "Selecci\u00f3 de pantalla" + } + }, + "title": "Toon" + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/en.json b/homeassistant/components/toon/.translations/en.json index 80d71d4e421..cea3146a3a5 100644 --- a/homeassistant/components/toon/.translations/en.json +++ b/homeassistant/components/toon/.translations/en.json @@ -1,34 +1,34 @@ { - "config": { - "title": "Toon", - "step": { - "authenticate": { - "title": "Link your Toon account", - "description": "Authenticate with your Eneco Toon account (not the developer account).", - "data": { - "username": "Username", - "password": "Password", - "tenant": "Tenant" - } - }, - "display": { - "title": "Select display", - "description": "Select the Toon display to connect with.", - "data": { - "display": "Choose display" - } - } - }, - "error": { - "credentials": "The provided credentials are invalid.", - "display_exists": "The selected display is already configured." - }, - "abort": { - "client_id": "The client ID from the configuration is invalid.", - "client_secret": "The client secret from the configuration is invalid.", - "unknown_auth_fail": "Unexpected error occured, while authenticating.", - "no_agreements": "This account has no Toon displays.", - "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/)." + "config": { + "abort": { + "client_id": "The client ID from the configuration is invalid.", + "client_secret": "The client secret from the configuration is invalid.", + "no_agreements": "This account has no Toon displays.", + "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/).", + "unknown_auth_fail": "Unexpected error occured, while authenticating." + }, + "error": { + "credentials": "The provided credentials are invalid.", + "display_exists": "The selected display is already configured." + }, + "step": { + "authenticate": { + "data": { + "password": "Password", + "tenant": "Tenant", + "username": "Username" + }, + "description": "Authenticate with your Eneco Toon account (not the developer account).", + "title": "Link your Toon account" + }, + "display": { + "data": { + "display": "Choose display" + }, + "description": "Select the Toon display to connect with.", + "title": "Select display" + } + }, + "title": "Toon" } - } } \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/es-419.json b/homeassistant/components/toon/.translations/es-419.json new file mode 100644 index 00000000000..db064def53b --- /dev/null +++ b/homeassistant/components/toon/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "unknown_auth_fail": "Ocurri\u00f3 un error inesperado, mientras se autenticaba." + }, + "error": { + "credentials": "Las credenciales proporcionadas no son v\u00e1lidas." + }, + "step": { + "authenticate": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/ko.json b/homeassistant/components/toon/.translations/ko.json new file mode 100644 index 00000000000..3a0698aed8e --- /dev/null +++ b/homeassistant/components/toon/.translations/ko.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "client_id": "\ud074\ub77c\uc774\uc5b8\ud2b8 ID \uac00 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "client_secret": "\ud074\ub77c\uc774\uc5b8\ud2b8 \ube44\ubc00\ubc88\ud638\uac00 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "no_agreements": "\uc774 \uacc4\uc815\uc5d0\ub294 Toon \ub514\uc2a4\ud50c\ub808\uc774\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", + "no_app": "Toon \uc744 \uc778\uc99d\ud558\uae30 \uc804\uc5d0 Toon \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/toon/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694.", + "unknown_auth_fail": "\uc778\uc99d\ud558\ub294 \ub3d9\uc548 \uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." + }, + "error": { + "credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "display_exists": "\uc120\ud0dd\ub41c \ub514\uc2a4\ud50c\ub808\uc774\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "authenticate": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "tenant": "\uac70\uc8fc\uc790", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "Eneco Toon \uacc4\uc815\uc73c\ub85c \uc778\uc99d\ud574\uc8fc\uc138\uc694. (\uac1c\ubc1c\uc790 \uacc4\uc815 \uc544\ub2d8)", + "title": "Toon \uacc4\uc815 \uc5f0\uacb0" + }, + "display": { + "data": { + "display": "\ub514\uc2a4\ud50c\ub808\uc774 \uc120\ud0dd" + }, + "description": "\uc5f0\uacb0\ud560 Toon \ub514\uc2a4\ud50c\ub808\uc774\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "\ub514\uc2a4\ud50c\ub808\uc774 \uc120\ud0dd" + } + }, + "title": "Toon" + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/lb.json b/homeassistant/components/toon/.translations/lb.json new file mode 100644 index 00000000000..6ea86c00057 --- /dev/null +++ b/homeassistant/components/toon/.translations/lb.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "client_id": "Client ID vun der Konfiguratioun ass ong\u00eblteg.", + "client_secret": "Client Passwuert vun der Konfiguratioun ass ong\u00eblteg.", + "no_agreements": "D\u00ebse Kont huet keen Toon Ecran.", + "no_app": "Dir musst Toon konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/toon/).", + "unknown_auth_fail": "Onerwaarte Feeler bei der Authentifikatioun." + }, + "error": { + "credentials": "Ong\u00eblteg Login Informatioune.", + "display_exists": "Den ausgewielten Ecran ass scho konfigur\u00e9iert." + }, + "step": { + "authenticate": { + "data": { + "password": "Passwuert", + "tenant": "Notzer", + "username": "Benotzernumm" + }, + "description": "Authentifikatioun mat \u00e4rem Eneco Toon Kont (net de Kont vum Entw\u00e9ckler)", + "title": "Toon Kont verbannnen" + }, + "display": { + "data": { + "display": "Ecran auswielen" + }, + "description": "Wielt den Toon Ecran aus fir sech domat ze verbannen.", + "title": "Ecran auswielen" + } + }, + "title": "Toon" + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json new file mode 100644 index 00000000000..0cc162218e9 --- /dev/null +++ b/homeassistant/components/toon/.translations/ru.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "client_id": "Client ID \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", + "client_secret": "Client secret \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", + "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.", + "no_app": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Toon \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/toon/).", + "unknown_auth_fail": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "display_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "step": { + "authenticate": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "tenant": "\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0441\u0432\u043e\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Eneco Toon (\u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430).", + "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Toon" + }, + "display": { + "data": { + "display": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u0438\u0441\u043f\u043b\u0435\u0439" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u0438\u0441\u043f\u043b\u0435\u0439 Toon \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "title": "Toon" + } + }, + "title": "Toon" + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/zh-Hant.json b/homeassistant/components/toon/.translations/zh-Hant.json new file mode 100644 index 00000000000..b09d921268c --- /dev/null +++ b/homeassistant/components/toon/.translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "client_id": "\u8a2d\u5b9a\u5167\u7528\u6236\u7aef ID \u7121\u6548\u3002", + "client_secret": "\u8a2d\u5b9a\u5167\u5ba2\u6236\u7aef\u5bc6\u78bc\u7121\u6548\u3002", + "no_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u88dd\u7f6e\u3002", + "no_app": "\u5fc5\u9808\u5148\u8a2d\u5b9a Toon \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/toon/\uff09\u3002", + "unknown_auth_fail": "\u9a57\u8b49\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" + }, + "error": { + "credentials": "\u6240\u63d0\u4f9b\u7684\u6191\u8b49\u7121\u6548\u3002", + "display_exists": "\u6240\u9078\u64c7\u7684\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "step": { + "authenticate": { + "data": { + "password": "\u5bc6\u78bc", + "tenant": "\u79df\u7528", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u4f7f\u7528 Eneco Toon \u5e33\u865f\uff08\u975e\u958b\u767c\u8005\u5e33\u865f\uff09\u9032\u884c\u9a57\u8b49\u3002", + "title": "\u9023\u7d50 Toon \u5e33\u865f" + }, + "display": { + "data": { + "display": "\u9078\u64c7\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u9023\u63a5\u7684 Toon display\u3002", + "title": "\u9078\u64c7\u88dd\u7f6e" + } + }, + "title": "Toon" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/ca.json b/homeassistant/components/tplink/.translations/ca.json new file mode 100644 index 00000000000..cf286f853f2 --- /dev/null +++ b/homeassistant/components/tplink/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius TP-Link a la xarxa.", + "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3." + }, + "step": { + "confirm": { + "description": "Vols configurar dispositius intel\u00b7ligents TP-Link?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/da.json b/homeassistant/components/tplink/.translations/da.json new file mode 100644 index 00000000000..cdd953ff5c3 --- /dev/null +++ b/homeassistant/components/tplink/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen TP-Link enheder kunne findes p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere TP-Link smart devices?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/de.json b/homeassistant/components/tplink/.translations/de.json new file mode 100644 index 00000000000..268d8ed0717 --- /dev/null +++ b/homeassistant/components/tplink/.translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Es wurden keine TP-Link-Ger\u00e4te im Netzwerk gefunden.", + "single_instance_allowed": "Es ist nur eine einzige Konfiguration erforderlich." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie TP-Link Smart Devices einrichten?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/es-419.json b/homeassistant/components/tplink/.translations/es-419.json new file mode 100644 index 00000000000..1d9fb41fc8c --- /dev/null +++ b/homeassistant/components/tplink/.translations/es-419.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/es.json b/homeassistant/components/tplink/.translations/es.json new file mode 100644 index 00000000000..9b6e34f6c35 --- /dev/null +++ b/homeassistant/components/tplink/.translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se han encontrado dispositivos TP-Link en la red.", + "single_instance_allowed": "S\u00f3lo es necesaria una configuraci\u00f3n." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar dispositivos de TP-Link?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/he.json b/homeassistant/components/tplink/.translations/he.json new file mode 100644 index 00000000000..094174b09c1 --- /dev/null +++ b/homeassistant/components/tplink/.translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9 TP-Link \u05d1\u05e8\u05e9\u05ea.", + "single_instance_allowed": "\u05e0\u05d3\u05e8\u05e9\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d1\u05dc\u05d1\u05d3" + }, + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d0\u05ea\u05d4 \u05e8\u05d5\u05e6\u05d4 \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d7\u05db\u05de\u05d9\u05dd \u05e9\u05dc TP-Link ?", + "title": "\u05d1\u05d9\u05ea \u05d7\u05db\u05dd \u05e9\u05dc TP-Link" + } + }, + "title": "\u05d1\u05d9\u05ea \u05d7\u05db\u05dd \u05e9\u05dc TP-Link" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/ko.json b/homeassistant/components/tplink/.translations/ko.json new file mode 100644 index 00000000000..c31e686a76d --- /dev/null +++ b/homeassistant/components/tplink/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "TP-Link \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 TP-Link \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "TP-Link \uc2a4\ub9c8\ud2b8 \uc7a5\uce58\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/lb.json b/homeassistant/components/tplink/.translations/lb.json new file mode 100644 index 00000000000..11ca7218e11 --- /dev/null +++ b/homeassistant/components/tplink/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng TP-Link Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun ass n\u00e9ideg." + }, + "step": { + "confirm": { + "description": "Soll TP-Link Smart Home konfigur\u00e9iert ginn?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/no.json b/homeassistant/components/tplink/.translations/no.json new file mode 100644 index 00000000000..4946eb81f02 --- /dev/null +++ b/homeassistant/components/tplink/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen TP-Link enheter funnet p\u00e5 nettverket.", + "single_instance_allowed": "Kun en enkelt konfigurasjon av TP-Link er n\u00f8dvendig." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere TP-Link smart enheter?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/pl.json b/homeassistant/components/tplink/.translations/pl.json new file mode 100644 index 00000000000..fa90495a5bf --- /dev/null +++ b/homeassistant/components/tplink/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 TP-Link w sieci.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja." + }, + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 urz\u0105dzenia TP-Link smart?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/ru.json b/homeassistant/components/tplink/.translations/ru.json new file mode 100644 index 00000000000..b7d76793245 --- /dev/null +++ b/homeassistant/components/tplink/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 TP-Link \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c TP-Link Smart Home?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/sv.json b/homeassistant/components/tplink/.translations/sv.json new file mode 100644 index 00000000000..14b6417d593 --- /dev/null +++ b/homeassistant/components/tplink/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga TP-Link enheter hittades p\u00e5 n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration \u00e4r n\u00f6dv\u00e4ndig." + }, + "step": { + "confirm": { + "description": "Vill du konfigurera TP-Link smart enheter?", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/zh-Hant.json b/homeassistant/components/tplink/.translations/zh-Hant.json new file mode 100644 index 00000000000..d44faf195e5 --- /dev/null +++ b/homeassistant/components/tplink/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 TP-Link \u88dd\u7f6e\u3002", + "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21\u5373\u53ef\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u88dd\u7f6e\uff1f", + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/ru.json b/homeassistant/components/twilio/.translations/ru.json index c195392be22..b8d6f11f7ef 100644 --- a/homeassistant/components/twilio/.translations/ru.json +++ b/homeassistant/components/twilio/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Twilio]({twilio_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Twilio]({twilio_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." }, "step": { "user": { diff --git a/homeassistant/components/twilio/.translations/sv.json b/homeassistant/components/twilio/.translations/sv.json new file mode 100644 index 00000000000..673997d5aa9 --- /dev/null +++ b/homeassistant/components/twilio/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot Twilio meddelanden.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera [Webhooks med Twilio]({twilio_url}).\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n Se [dokumentationen]({docs_url}) om hur du konfigurerar automatiseringar f\u00f6r att hantera inkommande data." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Twilio?", + "title": "Konfigurera Twilio Webhook" + } + }, + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/sv.json b/homeassistant/components/unifi/.translations/sv.json new file mode 100644 index 00000000000..864c887d6fe --- /dev/null +++ b/homeassistant/components/unifi/.translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Controller-platsen \u00e4r redan konfigurerad", + "user_privilege": "Anv\u00e4ndaren m\u00e5ste vara administrat\u00f6r" + }, + "error": { + "faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter", + "service_unavailable": "Ingen tj\u00e4nst tillg\u00e4nglig" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rddatorn", + "password": "L\u00f6senord", + "port": "Port", + "site": "Plats-ID", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Controller med korrekt certifikat" + }, + "title": "Konfigurera UniFi Controller" + } + }, + "title": "UniFi Controller" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/es.json b/homeassistant/components/upnp/.translations/es.json index 6afdeca8047..fa299cc379f 100644 --- a/homeassistant/components/upnp/.translations/es.json +++ b/homeassistant/components/upnp/.translations/es.json @@ -4,13 +4,19 @@ "already_configured": "UPnP / IGD ya est\u00e1 configurado", "incomplete_device": "Ignorando el dispositivo UPnP incompleto", "no_devices_discovered": "No se descubrieron UPnP / IGDs", - "no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos" + "no_devices_found": "No se encuentran dispositivos UPnP/IGD en la red.", + "no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos", + "single_instance_allowed": "S\u00f3lo se necesita una configuraci\u00f3n de UPnP/IGD." }, "error": { "one": "UNO", "other": "OTRO" }, "step": { + "confirm": { + "description": "\u00bfDesea configurar UPnP/IGD?", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP / IGD" }, diff --git a/homeassistant/components/upnp/.translations/sv.json b/homeassistant/components/upnp/.translations/sv.json index 63c63781845..e3864aee4da 100644 --- a/homeassistant/components/upnp/.translations/sv.json +++ b/homeassistant/components/upnp/.translations/sv.json @@ -2,14 +2,21 @@ "config": { "abort": { "already_configured": "UPnP/IGD \u00e4r redan konfigurerad", + "incomplete_device": "Ignorera ofullst\u00e4ndig UPnP-enhet", "no_devices_discovered": "Inga UPnP/IGDs uppt\u00e4cktes", - "no_sensors_or_port_mapping": "Aktivera minst sensorer eller portmappning" + "no_devices_found": "Inga UPnP/IGD-enheter hittades p\u00e5 n\u00e4tverket.", + "no_sensors_or_port_mapping": "Aktivera minst sensorer eller portmappning", + "single_instance_allowed": "Endast en enda konfiguration av UPnP/IGD \u00e4r n\u00f6dv\u00e4ndig." }, "error": { "one": "En", "other": "Andra" }, "step": { + "confirm": { + "description": "Vill du konfigurera UPnP/IGD?", + "title": "UPnP/IGD" + }, "init": { "title": "UPnP/IGD" }, diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json index 280c941b427..686c1f35a98 100644 --- a/homeassistant/components/zha/.translations/de.json +++ b/homeassistant/components/zha/.translations/de.json @@ -12,6 +12,7 @@ "radio_type": "Radio-Type", "usb_path": "USB-Ger\u00e4te-Pfad" }, + "description": "Leer", "title": "ZHA" } }, diff --git a/homeassistant/components/zha/.translations/es.json b/homeassistant/components/zha/.translations/es.json new file mode 100644 index 00000000000..9984a316884 --- /dev/null +++ b/homeassistant/components/zha/.translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de ZHA." + }, + "error": { + "cannot_connect": "No se puede conectar al dispositivo ZHA." + }, + "step": { + "user": { + "data": { + "radio_type": "Tipo de radio", + "usb_path": "Ruta del dispositivo USB" + }, + "description": "Vac\u00edo", + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/sv.json b/homeassistant/components/zha/.translations/sv.json new file mode 100644 index 00000000000..029f0391657 --- /dev/null +++ b/homeassistant/components/zha/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Endast en enda konfiguration av ZHA \u00e4r till\u00e5ten." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta till ZHA enhet." + }, + "step": { + "user": { + "data": { + "radio_type": "Typ av radio", + "usb_path": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "?", + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file From 1ef3e32d4afde26f50f00b033c5b53139e390066 Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Thu, 28 Feb 2019 11:38:53 +1100 Subject: [PATCH 211/253] ness_alarm: Bump nessclient version to 0.9.13 (#21466) --- homeassistant/components/ness_alarm/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index 8dcb9541597..c7175a0c3c7 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -10,7 +10,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send -REQUIREMENTS = ['nessclient==0.9.10'] +REQUIREMENTS = ['nessclient==0.9.13'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 52382f1de06..d464de86489 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -722,7 +722,7 @@ nanoleaf==0.4.1 ndms2_client==0.0.6 # homeassistant.components.ness_alarm -nessclient==0.9.10 +nessclient==0.9.13 # homeassistant.components.sensor.netdata netdata==0.1.2 From 070320a24afeba1d336b24dc16aa4c0e6010f69c Mon Sep 17 00:00:00 2001 From: msvinth Date: Thu, 28 Feb 2019 00:39:11 +0000 Subject: [PATCH 212/253] Bump ihc to 2.3.0 (#21494) --- homeassistant/components/ihc/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 3365942df45..bd45a52944c 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType -REQUIREMENTS = ['ihcsdk==2.2.0', 'defusedxml==0.5.0'] +REQUIREMENTS = ['ihcsdk==2.3.0', 'defusedxml==0.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d464de86489..e4336a03ffe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -568,7 +568,7 @@ ibmiotf==0.3.4 iglo==1.2.7 # homeassistant.components.ihc -ihcsdk==2.2.0 +ihcsdk==2.3.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 4a45510c882d7ccbcd764e7e11af81e117595eaf Mon Sep 17 00:00:00 2001 From: Victor Vostrikov <1998617+gorynychzmey@users.noreply.github.com> Date: Thu, 28 Feb 2019 01:40:23 +0100 Subject: [PATCH 213/253] Changed source priority for Person (#21479) * Added gps accuracy to Person * Corrected GPS accuracy for Person * Added priority of sources to Person * Fixed formatting * Removed rounding of coordinates. * Added test for source priority. Changed test for rounding of coordinates. * Improved code style * Code style cleanup * Code style cleanup * Code style cleanup * Code style cleanup * Code style cleanup * Lint * Lint --- homeassistant/components/person/__init__.py | 38 +++++++++++--- tests/components/person/test_init.py | 55 ++++++++++++++++----- 2 files changed, 74 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index f2bca91205c..c4af4a699cd 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -8,10 +8,11 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.device_tracker import ( - DOMAIN as DEVICE_TRACKER_DOMAIN) + DOMAIN as DEVICE_TRACKER_DOMAIN, ATTR_SOURCE_TYPE, SOURCE_TYPE_GPS) from homeassistant.const import ( - ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ID, CONF_NAME, - EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, STATE_UNAVAILABLE) + ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_GPS_ACCURACY, + CONF_ID, CONF_NAME, EVENT_HOMEASSISTANT_START, + STATE_UNKNOWN, STATE_UNAVAILABLE, STATE_HOME, STATE_NOT_HOME) from homeassistant.core import callback, Event from homeassistant.auth import EVENT_USER_REMOVED import homeassistant.helpers.config_validation as cv @@ -285,6 +286,7 @@ class Person(RestoreEntity): self._editable = editable self._latitude = None self._longitude = None + self._gps_accuracy = None self._source = None self._state = None self._unsub_track_device = None @@ -315,9 +317,11 @@ class Person(RestoreEntity): ATTR_ID: self.unique_id, } if self._latitude is not None: - data[ATTR_LATITUDE] = round(self._latitude, 5) + data[ATTR_LATITUDE] = self._latitude if self._longitude is not None: - data[ATTR_LONGITUDE] = round(self._longitude, 5) + data[ATTR_LONGITUDE] = self._longitude + if self._gps_accuracy is not None: + data[ATTR_GPS_ACCURACY] = self._gps_accuracy if self._source is not None: data[ATTR_SOURCE] = self._source user_id = self._config.get(CONF_USER_ID) @@ -373,18 +377,34 @@ class Person(RestoreEntity): """Handle the device tracker state changes.""" self._update_state() + def _get_latest(self, prev, curr): + return curr \ + if prev is None or curr.last_updated > prev.last_updated \ + else prev + @callback def _update_state(self): """Update the state.""" - latest = None + latest_home = latest_not_home = latest_gps = latest = None for entity_id in self._config.get(CONF_DEVICE_TRACKERS, []): state = self.hass.states.get(entity_id) if not state or state.state in IGNORE_STATES: continue - if latest is None or state.last_updated > latest.last_updated: - latest = state + if state.attributes.get(ATTR_SOURCE_TYPE) == SOURCE_TYPE_GPS: + latest_gps = self._get_latest(latest_gps, state) + elif state.state == STATE_HOME: + latest_home = self._get_latest(latest_home, state) + elif state.state == STATE_NOT_HOME: + latest_not_home = self._get_latest(latest_not_home, state) + + if latest_home: + latest = latest_home + elif latest_gps: + latest = latest_gps + else: + latest = latest_not_home if latest: self._parse_source_state(latest) @@ -393,6 +413,7 @@ class Person(RestoreEntity): self._source = None self._latitude = None self._longitude = None + self._gps_accuracy = None self.async_schedule_update_ha_state() @@ -406,6 +427,7 @@ class Person(RestoreEntity): self._source = state.entity_id self._latitude = state.attributes.get(ATTR_LATITUDE) self._longitude = state.attributes.get(ATTR_LONGITUDE) + self._gps_accuracy = state.attributes.get(ATTR_GPS_ACCURACY) @websocket_api.websocket_command({ diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 6c8c6ebd0dd..ef129a555be 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -4,8 +4,10 @@ from unittest.mock import Mock from homeassistant.components.person import ( ATTR_SOURCE, ATTR_USER_ID, DOMAIN, PersonManager) from homeassistant.const import ( - ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, STATE_UNKNOWN, - EVENT_HOMEASSISTANT_START) + ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_GPS_ACCURACY, + STATE_UNKNOWN, EVENT_HOMEASSISTANT_START) +from homeassistant.components.device_tracker import ( + ATTR_SOURCE_TYPE, SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER) from homeassistant.core import CoreState, State from homeassistant.setup import async_setup_component @@ -134,15 +136,18 @@ async def test_setup_tracker(hass, hass_admin_user): assert state.attributes.get(ATTR_USER_ID) == user_id hass.states.async_set( - DEVICE_TRACKER, 'not_home', - {ATTR_LATITUDE: 10.123456, ATTR_LONGITUDE: 11.123456}) + DEVICE_TRACKER, 'not_home', { + ATTR_LATITUDE: 10.123456, + ATTR_LONGITUDE: 11.123456, + ATTR_GPS_ACCURACY: 10}) await hass.async_block_till_done() state = hass.states.get('person.tracked_person') assert state.state == 'not_home' assert state.attributes.get(ATTR_ID) == '1234' - assert state.attributes.get(ATTR_LATITUDE) == 10.12346 - assert state.attributes.get(ATTR_LONGITUDE) == 11.12346 + assert state.attributes.get(ATTR_LATITUDE) == 10.123456 + assert state.attributes.get(ATTR_LONGITUDE) == 11.123456 + assert state.attributes.get(ATTR_GPS_ACCURACY) == 10 assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER assert state.attributes.get(ATTR_USER_ID) == user_id @@ -166,7 +171,8 @@ async def test_setup_two_trackers(hass, hass_admin_user): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - hass.states.async_set(DEVICE_TRACKER, 'home') + hass.states.async_set( + DEVICE_TRACKER, 'home', {ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER}) await hass.async_block_till_done() state = hass.states.get('person.tracked_person') @@ -174,22 +180,49 @@ async def test_setup_two_trackers(hass, hass_admin_user): assert state.attributes.get(ATTR_ID) == '1234' assert state.attributes.get(ATTR_LATITUDE) is None assert state.attributes.get(ATTR_LONGITUDE) is None + assert state.attributes.get(ATTR_GPS_ACCURACY) is None assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER assert state.attributes.get(ATTR_USER_ID) == user_id hass.states.async_set( - DEVICE_TRACKER_2, 'not_home', - {ATTR_LATITUDE: 12.123456, ATTR_LONGITUDE: 13.123456}) + DEVICE_TRACKER_2, 'not_home', { + ATTR_LATITUDE: 12.123456, + ATTR_LONGITUDE: 13.123456, + ATTR_GPS_ACCURACY: 12, + ATTR_SOURCE_TYPE: SOURCE_TYPE_GPS}) + await hass.async_block_till_done() + hass.states.async_set( + DEVICE_TRACKER, 'not_home', {ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER}) await hass.async_block_till_done() state = hass.states.get('person.tracked_person') assert state.state == 'not_home' assert state.attributes.get(ATTR_ID) == '1234' - assert state.attributes.get(ATTR_LATITUDE) == 12.12346 - assert state.attributes.get(ATTR_LONGITUDE) == 13.12346 + assert state.attributes.get(ATTR_LATITUDE) == 12.123456 + assert state.attributes.get(ATTR_LONGITUDE) == 13.123456 + assert state.attributes.get(ATTR_GPS_ACCURACY) == 12 assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER_2 assert state.attributes.get(ATTR_USER_ID) == user_id + hass.states.async_set( + DEVICE_TRACKER_2, 'zone1', {ATTR_SOURCE_TYPE: SOURCE_TYPE_GPS}) + await hass.async_block_till_done() + + state = hass.states.get('person.tracked_person') + assert state.state == 'zone1' + assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER_2 + + hass.states.async_set( + DEVICE_TRACKER, 'home', {ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER}) + await hass.async_block_till_done() + hass.states.async_set( + DEVICE_TRACKER_2, 'zone2', {ATTR_SOURCE_TYPE: SOURCE_TYPE_GPS}) + await hass.async_block_till_done() + + state = hass.states.get('person.tracked_person') + assert state.state == 'home' + assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER + async def test_ignore_unavailable_states(hass, hass_admin_user): """Test set up person with two device trackers, one unavailable.""" From 45bbe75d296ea28d3a4edd12aab3e07c6a3fca3c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Feb 2019 16:45:14 -0800 Subject: [PATCH 214/253] Bumped version to 0.89.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 54901feb73b..0af0b220ecc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 89 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 1c889cfcc36e196c332e99eea647f6d6f2d03aa2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Feb 2019 17:43:36 -0800 Subject: [PATCH 215/253] Updated frontend to 20190228.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 7ef030e87d7..614b5228f60 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190227.0'] +REQUIREMENTS = ['home-assistant-frontend==20190228.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index e4336a03ffe..780addec717 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190227.0 +home-assistant-frontend==20190228.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index deed85b20ff..6c2a15e59d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -116,7 +116,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190227.0 +home-assistant-frontend==20190228.0 # homeassistant.components.homekit_controller homekit==0.12.2 From e6cbdf0645bf4500ada5b4b1508217f5ae538025 Mon Sep 17 00:00:00 2001 From: Ben Randall Date: Thu, 28 Feb 2019 09:27:40 -0800 Subject: [PATCH 216/253] Add PLATFORM_SCHEMA_BASE to telegram_bot component (#21155) --- homeassistant/components/telegram_bot/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index c55b27e97a6..78d45535c48 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -84,6 +84,8 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PROXY_PARAMS): dict, }) +PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) + BASE_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(ATTR_PARSER): cv.string, From 9e140d27bf2c37248daed59b1e9b35eed80a4d82 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 27 Feb 2019 21:04:55 +0100 Subject: [PATCH 217/253] Fix deCONZ retry mechanism for setup From aa546b5a1f25b3651b95fc4b8f1619b0900ea044 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Feb 2019 15:28:20 -0700 Subject: [PATCH 218/253] Add watchdog to Ambient PWS (#21507) * Add watchdog to Ambient PWS * Better labeling * Owner comments --- .../components/ambient_station/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index a8e1c7fb292..70f6ce9fbba 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -27,6 +27,7 @@ _LOGGER = logging.getLogger(__name__) DATA_CONFIG = 'config' DEFAULT_SOCKET_MIN_RETRY = 15 +DEFAULT_WATCHDOG_SECONDS = 5 * 60 TYPE_24HOURRAININ = '24hourrainin' TYPE_BAROMABSIN = 'baromabsin' @@ -296,6 +297,7 @@ class AmbientStation: """Initialize.""" self._config_entry = config_entry self._hass = hass + self._watchdog_listener = None self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY self.client = client self.monitored_conditions = monitored_conditions @@ -305,9 +307,18 @@ class AmbientStation: """Register handlers and connect to the websocket.""" from aioambient.errors import WebsocketError + async def _ws_reconnect(event_time): + """Forcibly disconnect from and reconnect to the websocket.""" + _LOGGER.debug('Watchdog expired; forcing socket reconnection') + await self.client.websocket.disconnect() + await self.client.websocket.connect() + def on_connect(): """Define a handler to fire when the websocket is connected.""" _LOGGER.info('Connected to websocket') + _LOGGER.debug('Watchdog starting') + self._watchdog_listener = async_call_later( + self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect) def on_data(data): """Define a handler to fire when the data is received.""" @@ -317,6 +328,11 @@ class AmbientStation: self.stations[mac_address][ATTR_LAST_DATA] = data async_dispatcher_send(self._hass, TOPIC_UPDATE) + _LOGGER.debug('Resetting watchdog') + self._watchdog_listener() + self._watchdog_listener = async_call_later( + self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect) + def on_disconnect(): """Define a handler to fire when the websocket is disconnected.""" _LOGGER.info('Disconnected from websocket') From 26a534a67c42f7d7a84633121d168425219e3fa6 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 28 Feb 2019 19:25:31 +0100 Subject: [PATCH 219/253] Improve new Sonos snapshot/restore (#21509) * Fine-tune new Sonos snapshot/restore * Move into class --- .../components/sonos/media_player.py | 145 ++++++++++-------- tests/components/sonos/test_media_player.py | 4 +- 2 files changed, 86 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 4a02cf2676f..e0f881f723d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -197,9 +197,11 @@ def _setup_platform(hass, config, add_entities, discovery_info): with hass.data[DATA_SONOS].topology_lock: if service.service == SERVICE_SNAPSHOT: - snapshot(entities, service.data[ATTR_WITH_GROUP]) + SonosEntity.snapshot_multi( + entities, service.data[ATTR_WITH_GROUP]) elif service.service == SERVICE_RESTORE: - restore(entities, service.data[ATTR_WITH_GROUP]) + SonosEntity.restore_multi( + entities, service.data[ATTR_WITH_GROUP]) elif service.service == SERVICE_JOIN: master = [e for e in hass.data[DATA_SONOS].entities if e.entity_id == service.data[ATTR_MASTER]] @@ -357,6 +359,7 @@ class SonosEntity(MediaPlayerDevice): self._favorites = None self._soco_snapshot = None self._snapshot_group = None + self._restore_pending = False self._set_basic_information() @@ -724,6 +727,9 @@ class SonosEntity(MediaPlayerDevice): pass if self.unique_id == coordinator_uid: + if self._restore_pending: + self.restore() + sonos_group = [] for uid in (coordinator_uid, *slave_uids): entity = _get_entity_from_soco_uid(self.hass, uid) @@ -974,6 +980,82 @@ class SonosEntity(MediaPlayerDevice): self.soco.unjoin() self._coordinator = None + @soco_error() + def snapshot(self, with_group): + """Snapshot the state of a player.""" + from pysonos.snapshot import Snapshot + + self._soco_snapshot = Snapshot(self.soco) + self._soco_snapshot.snapshot() + if with_group: + self._snapshot_group = self._sonos_group.copy() + else: + self._snapshot_group = None + + @soco_error() + def restore(self): + """Restore a snapshotted state to a player.""" + from pysonos.exceptions import SoCoException + + try: + # pylint: disable=protected-access + self.soco._zgs_cache.clear() + self._soco_snapshot.restore() + except (TypeError, AttributeError, SoCoException) as ex: + # Can happen if restoring a coordinator onto a current slave + _LOGGER.warning("Error on restore %s: %s", self.entity_id, ex) + + self._soco_snapshot = None + self._snapshot_group = None + self._restore_pending = False + + @staticmethod + def snapshot_multi(entities, with_group): + """Snapshot all the entities and optionally their groups.""" + # pylint: disable=protected-access + # Find all affected players + entities = set(entities) + if with_group: + for entity in list(entities): + entities.update(entity._sonos_group) + + for entity in entities: + entity.snapshot(with_group) + + @staticmethod + def restore_multi(entities, with_group): + """Restore snapshots for all the entities.""" + # pylint: disable=protected-access + # Find all affected players + entities = set(e for e in entities if e._soco_snapshot) + if with_group: + for entity in [e for e in entities if e._snapshot_group]: + entities.update(entity._snapshot_group) + + # Pause all current coordinators + for entity in (e for e in entities if e.is_coordinator): + if entity.state == STATE_PLAYING: + entity.media_pause() + + # Bring back the original group topology + if with_group: + for entity in (e for e in entities if e._snapshot_group): + if entity._snapshot_group[0] == entity: + entity.join(entity._snapshot_group) + + # Restore slaves + for entity in (e for e in entities if not e.is_coordinator): + entity.restore() + + # Restore coordinators (or delay if moving from slave) + for entity in (e for e in entities if e.is_coordinator): + if entity._sonos_group[0] == entity: + # Was already coordinator + entity.restore() + else: + # Await coordinator role + entity._restore_pending = True + @soco_error() @soco_coordinator def set_sleep_timer(self, sleep_time): @@ -1033,62 +1115,3 @@ class SonosEntity(MediaPlayerDevice): attributes[ATTR_SPEECH_ENHANCE] = self._speech_enhance return attributes - - -@soco_error() -def snapshot(entities, with_group): - """Snapshot all the entities and optionally their groups.""" - # pylint: disable=protected-access - from pysonos.snapshot import Snapshot - - # Find all affected players - entities = set(entities) - if with_group: - for entity in list(entities): - entities.update(entity._sonos_group) - - for entity in entities: - entity._soco_snapshot = Snapshot(entity.soco) - entity._soco_snapshot.snapshot() - if with_group: - entity._snapshot_group = entity._sonos_group.copy() - else: - entity._snapshot_group = None - - -@soco_error() -def restore(entities, with_group): - """Restore snapshots for all the entities.""" - # pylint: disable=protected-access - from pysonos.exceptions import SoCoException - - # Find all affected players - entities = set(e for e in entities if e._soco_snapshot) - if with_group: - for entity in [e for e in entities if e._snapshot_group]: - entities.update(entity._snapshot_group) - - # Pause all current coordinators - for entity in (e for e in entities if e.is_coordinator): - if entity.state == STATE_PLAYING: - entity.media_pause() - - # Bring back the original group topology and clear pysonos cache - if with_group: - for entity in (e for e in entities if e._snapshot_group): - if entity._snapshot_group[0] == entity: - entity.join(entity._snapshot_group) - entity.soco._zgs_cache.clear() - - # Restore slaves, then coordinators - slaves = [e for e in entities if not e.is_coordinator] - coordinators = [e for e in entities if e.is_coordinator] - for entity in slaves + coordinators: - try: - entity._soco_snapshot.restore() - except (TypeError, AttributeError, SoCoException) as ex: - # Can happen if restoring a coordinator onto a current slave - _LOGGER.warning("Error on restore %s: %s", entity.entity_id, ex) - - entity._soco_snapshot = None - entity._snapshot_group = None diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 55743c4f843..798c92eddad 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -328,7 +328,7 @@ class TestSonosMediaPlayer(unittest.TestCase): snapshotMock.return_value = True entity.soco.group = mock.MagicMock() entity.soco.group.members = [e.soco for e in entities] - sonos.snapshot(entities, True) + sonos.SonosEntity.snapshot_multi(entities, True) assert snapshotMock.call_count == 1 assert snapshotMock.call_args == mock.call() @@ -350,6 +350,6 @@ class TestSonosMediaPlayer(unittest.TestCase): entity._snapshot_group = mock.MagicMock() entity._snapshot_group.members = [e.soco for e in entities] entity._soco_snapshot = Snapshot(entity.soco) - sonos.restore(entities, True) + sonos.SonosEntity.restore_multi(entities, True) assert restoreMock.call_count == 1 assert restoreMock.call_args == mock.call() From 4fe9f966adba17d5eff3bf823181445944a6e957 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Feb 2019 21:35:14 -0800 Subject: [PATCH 220/253] Fix lint (#21520) --- homeassistant/components/person/__init__.py | 21 +++++++++++--------- homeassistant/components/sensor/airvisual.py | 4 ++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index c4af4a699cd..622ca0608ac 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -2,6 +2,7 @@ from collections import OrderedDict from itertools import chain import logging +from typing import Optional import uuid import voluptuous as vol @@ -13,7 +14,7 @@ from homeassistant.const import ( ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_GPS_ACCURACY, CONF_ID, CONF_NAME, EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, STATE_UNAVAILABLE, STATE_HOME, STATE_NOT_HOME) -from homeassistant.core import callback, Event +from homeassistant.core import callback, Event, State from homeassistant.auth import EVENT_USER_REMOVED import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent @@ -377,11 +378,6 @@ class Person(RestoreEntity): """Handle the device tracker state changes.""" self._update_state() - def _get_latest(self, prev, curr): - return curr \ - if prev is None or curr.last_updated > prev.last_updated \ - else prev - @callback def _update_state(self): """Update the state.""" @@ -393,11 +389,11 @@ class Person(RestoreEntity): continue if state.attributes.get(ATTR_SOURCE_TYPE) == SOURCE_TYPE_GPS: - latest_gps = self._get_latest(latest_gps, state) + latest_gps = _get_latest(latest_gps, state) elif state.state == STATE_HOME: - latest_home = self._get_latest(latest_home, state) + latest_home = _get_latest(latest_home, state) elif state.state == STATE_NOT_HOME: - latest_not_home = self._get_latest(latest_not_home, state) + latest_not_home = _get_latest(latest_not_home, state) if latest_home: latest = latest_home @@ -508,3 +504,10 @@ async def ws_delete_person(hass: HomeAssistantType, manager = hass.data[DOMAIN] # type: PersonManager await manager.async_delete_person(msg['person_id']) connection.send_result(msg['id']) + + +def _get_latest(prev: Optional[State], curr: State): + """Get latest state.""" + if prev is None or curr.last_updated > prev.last_updated: + return curr + return prev diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py index 46457a17ebb..e13fb924041 100644 --- a/homeassistant/components/sensor/airvisual.py +++ b/homeassistant/components/sensor/airvisual.py @@ -141,7 +141,7 @@ async def async_setup_platform( "Using city, state, and country: %s, %s, %s", city, state, country) location_id = ','.join((city, state, country)) data = AirVisualData( - Client(config[CONF_API_KEY], websession), + Client(websession, api_key=config[CONF_API_KEY]), city=city, state=state, country=country, @@ -152,7 +152,7 @@ async def async_setup_platform( "Using latitude and longitude: %s, %s", latitude, longitude) location_id = ','.join((str(latitude), str(longitude))) data = AirVisualData( - Client(config[CONF_API_KEY], websession), + Client(websession, api_key=config[CONF_API_KEY]), latitude=latitude, longitude=longitude, show_on_map=config[CONF_SHOW_ON_MAP], From 238c4247d91a8a057e6db81adb9fcc65efe3a01e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Feb 2019 10:00:17 -0800 Subject: [PATCH 221/253] Only use a single store instance (#21521) --- homeassistant/components/frontend/storage.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index f01abc79e8e..17aae14c820 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -11,7 +11,7 @@ STORAGE_KEY_USER_DATA = 'frontend.user_data_{}' async def async_setup_frontend_storage(hass): """Set up frontend storage.""" - hass.data[DATA_STORAGE] = {} + hass.data[DATA_STORAGE] = ({}, {}) hass.components.websocket_api.async_register_command( websocket_set_user_data ) @@ -25,12 +25,16 @@ def with_store(orig_func): @wraps(orig_func) async def with_store_func(hass, connection, msg): """Provide user specific data and store to function.""" - store = hass.helpers.storage.Store( - STORAGE_VERSION_USER_DATA, - STORAGE_KEY_USER_DATA.format(connection.user.id) - ) - data = hass.data[DATA_STORAGE] + stores, data = hass.data[DATA_STORAGE] user_id = connection.user.id + store = stores.get(user_id) + + if store is None: + store = stores[user_id] = hass.helpers.storage.Store( + STORAGE_VERSION_USER_DATA, + STORAGE_KEY_USER_DATA.format(connection.user.id) + ) + if user_id not in data: data[user_id] = await store.async_load() or {} From eda2290d473d96314b410235d437fb75ca1a7a27 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 28 Feb 2019 10:01:10 -0800 Subject: [PATCH 222/253] Allow skip-pip applied to HA core (#21527) --- homeassistant/bootstrap.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index ca01610bcf9..eef36b026e1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -85,6 +85,11 @@ async def async_from_config_dict(config: Dict[str, Any], async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) + hass.config.skip_pip = skip_pip + if skip_pip: + _LOGGER.warning("Skipping pip installation of required modules. " + "This may cause issues") + core_config = config.get(core.DOMAIN, {}) has_api_password = bool(config.get('http', {}).get('api_password')) trusted_networks = config.get('http', {}).get('trusted_networks') @@ -104,11 +109,6 @@ async def async_from_config_dict(config: Dict[str, Any], await hass.async_add_executor_job( conf_util.process_ha_config_upgrade, hass) - hass.config.skip_pip = skip_pip - if skip_pip: - _LOGGER.warning("Skipping pip installation of required modules. " - "This may cause issues") - # Make a copy because we are mutating it. config = OrderedDict(config) From 6f2dd21516c0858f2f07438f2a93a8b67489570a Mon Sep 17 00:00:00 2001 From: Victor Vostrikov <1998617+gorynychzmey@users.noreply.github.com> Date: Thu, 28 Feb 2019 13:05:39 +0100 Subject: [PATCH 223/253] Updated variable name for readability (#21528) --- homeassistant/components/person/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 622ca0608ac..e6f83b80ba4 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -381,7 +381,7 @@ class Person(RestoreEntity): @callback def _update_state(self): """Update the state.""" - latest_home = latest_not_home = latest_gps = latest = None + latest_non_gps_home = latest_not_home = latest_gps = latest = None for entity_id in self._config.get(CONF_DEVICE_TRACKERS, []): state = self.hass.states.get(entity_id) @@ -391,12 +391,12 @@ class Person(RestoreEntity): if state.attributes.get(ATTR_SOURCE_TYPE) == SOURCE_TYPE_GPS: latest_gps = _get_latest(latest_gps, state) elif state.state == STATE_HOME: - latest_home = _get_latest(latest_home, state) + latest_non_gps_home = _get_latest(latest_non_gps_home, state) elif state.state == STATE_NOT_HOME: latest_not_home = _get_latest(latest_not_home, state) - if latest_home: - latest = latest_home + if latest_non_gps_home: + latest = latest_non_gps_home elif latest_gps: latest = latest_gps else: From b05062e9d99eecfbf17f17e4ba8718d4d682a350 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Thu, 28 Feb 2019 18:26:54 +0100 Subject: [PATCH 224/253] Add missing retain option to mqtt.climate configuration schema (#21536) --- homeassistant/components/mqtt/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 957e1aadfb7..7be47185322 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -93,6 +93,7 @@ TEMPLATE_KEYS = ( SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema) PLATFORM_SCHEMA = SCHEMA_BASE.extend({ + vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_TEMPERATURE_COMMAND_TOPIC): mqtt.valid_publish_topic, From 97b93bcf7bb378f409363bef58d37b53f19776da Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 28 Feb 2019 10:10:21 -0800 Subject: [PATCH 225/253] Fix warning (#21538) --- homeassistant/components/http/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index f57068081a5..4928ae2ab17 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -55,6 +55,9 @@ NO_LOGIN_ATTEMPT_THRESHOLD = -1 def trusted_networks_deprecated(value): """Warn user trusted_networks config is deprecated.""" + if not value: + return value + _LOGGER.warning( "Configuring trusted_networks via the http component has been" " deprecated. Use the trusted networks auth provider instead." From 0f09c0287527005ecf72a7a50b15b2c0ff4b4559 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Feb 2019 15:17:10 -0700 Subject: [PATCH 226/253] Fix incorrect pyairvisual call (#21542) --- homeassistant/components/sensor/airvisual.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py index e13fb924041..b9e7a3315e3 100644 --- a/homeassistant/components/sensor/airvisual.py +++ b/homeassistant/components/sensor/airvisual.py @@ -278,11 +278,11 @@ class AirVisualData: try: if self.city and self.state and self.country: - resp = await self._client.data.city( + resp = await self._client.api.city( self.city, self.state, self.country) self.longitude, self.latitude = resp['location']['coordinates'] else: - resp = await self._client.data.nearest_city( + resp = await self._client.api.nearest_city( self.latitude, self.longitude) _LOGGER.debug("New data retrieved: %s", resp) From ed28482311f053b173310daec0a3a6b98aa36124 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Feb 2019 17:58:23 -0800 Subject: [PATCH 227/253] Bumped version to 0.89.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0af0b220ecc..a3dfff9a0b0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 89 -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 43f85f70536a3cab9c9b3566d890c50348e0d04e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Mar 2019 21:55:51 -0800 Subject: [PATCH 228/253] Updated frontend to 20190303.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 614b5228f60..fbbea13f026 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190228.0'] +REQUIREMENTS = ['home-assistant-frontend==20190303.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index 780addec717..3ede6e21e36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190228.0 +home-assistant-frontend==20190303.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c2a15e59d2..2892b759c99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -116,7 +116,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190228.0 +home-assistant-frontend==20190303.0 # homeassistant.components.homekit_controller homekit==0.12.2 From 73675d5a4840e8048eff370f2c5443cf74f68fa4 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 1 Mar 2019 23:08:20 -0800 Subject: [PATCH 229/253] mobile_app component (#21475) * Initial pass of a mobile_app component * Fully support encryption, validation for the webhook payloads, and other general improvements * Return same format as original API calls * Minor encryption fixes, logging improvements * Migrate Owntracks to use the superior PyNaCl instead of libnacl, mark it as a requirement in mobile_app * Add mobile_app to .coveragerc * Dont manually b64decode on OT * Initial requested changes * Round two of fixes * Initial mobile_app tests * Dont allow making registration requests for same/existing device * Test formatting fixes * Add mobile_app to default_config * Add some more keys allowed in registration payloads * Add support for getting a single device, updating a device, getting all devices. Also change from /api/mobile_app/register to /api/mobile_app/devices * Change device_id to fingerprint * Next round of changes * Add keyword args and pass context on all relevant calls * Remove SingleDeviceView in favor of webhook type to update registration * Only allow some properties to be updated on registrations, rename integration_data to app_data * Add call service test, ensure events actually fire, only run the encryption tests if sodium is installed * pylint * Fix OwnTracks test * Fix iteration of devices and remove device_for_webhook_id --- .coveragerc | 3 +- .../components/default_config/__init__.py | 1 + .../components/mobile_app/__init__.py | 355 ++++++++++++++++++ .../components/owntracks/__init__.py | 2 +- .../components/owntracks/config_flow.py | 2 +- .../components/owntracks/device_tracker.py | 10 +- requirements_all.txt | 7 +- requirements_test_all.txt | 4 + script/gen_requirements_all.py | 1 + .../device_tracker/test_owntracks.py | 26 +- tests/components/mobile_app/__init__.py | 1 + tests/components/mobile_app/test_init.py | 275 ++++++++++++++ 12 files changed, 666 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/mobile_app/__init__.py create mode 100644 tests/components/mobile_app/__init__.py create mode 100644 tests/components/mobile_app/test_init.py diff --git a/.coveragerc b/.coveragerc index f8829939682..bbed9b7e742 100644 --- a/.coveragerc +++ b/.coveragerc @@ -320,6 +320,7 @@ omit = homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/ziggo_mediabox_xl.py homeassistant/components/meteo_france/* + homeassistant/components/mobile_app/* homeassistant/components/mochad/* homeassistant/components/modbus/* homeassistant/components/mychevy/* @@ -384,7 +385,7 @@ omit = homeassistant/components/point/* homeassistant/components/prometheus/* homeassistant/components/ps4/__init__.py - homeassistant/components/ps4/media_player.py + homeassistant/components/ps4/media_player.py homeassistant/components/qwikswitch/* homeassistant/components/rachio/* homeassistant/components/rainbird/* diff --git a/homeassistant/components/default_config/__init__.py b/homeassistant/components/default_config/__init__.py index d56cf9a4ee8..badc403c7c8 100644 --- a/homeassistant/components/default_config/__init__.py +++ b/homeassistant/components/default_config/__init__.py @@ -11,6 +11,7 @@ DEPENDENCIES = ( 'history', 'logbook', 'map', + 'mobile_app', 'person', 'script', 'sun', diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py new file mode 100644 index 00000000000..19a81b4aa45 --- /dev/null +++ b/homeassistant/components/mobile_app/__init__.py @@ -0,0 +1,355 @@ +"""Support for native mobile apps.""" +import logging +import json +from functools import partial + +import voluptuous as vol +from aiohttp.web import json_response, Response +from aiohttp.web_exceptions import HTTPBadRequest + +from homeassistant import config_entries +from homeassistant.auth.util import generate_secret +import homeassistant.core as ha +from homeassistant.core import Context +from homeassistant.components import webhook +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER_DOMAIN, SERVICE_SEE as DEVICE_TRACKER_SEE, + SERVICE_SEE_PAYLOAD_SCHEMA as SEE_SCHEMA) +from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, + HTTP_BAD_REQUEST, HTTP_CREATED, + HTTP_INTERNAL_SERVER_ERROR, CONF_WEBHOOK_ID) +from homeassistant.exceptions import (HomeAssistantError, ServiceNotFound, + TemplateError) +from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.typing import HomeAssistantType + +REQUIREMENTS = ['PyNaCl==1.3.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'mobile_app' + +DEPENDENCIES = ['device_tracker', 'http', 'webhook'] + +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + +CONF_SECRET = 'secret' +CONF_USER_ID = 'user_id' + +ATTR_APP_DATA = 'app_data' +ATTR_APP_ID = 'app_id' +ATTR_APP_NAME = 'app_name' +ATTR_APP_VERSION = 'app_version' +ATTR_DEVICE_NAME = 'device_name' +ATTR_MANUFACTURER = 'manufacturer' +ATTR_MODEL = 'model' +ATTR_OS_VERSION = 'os_version' +ATTR_SUPPORTS_ENCRYPTION = 'supports_encryption' + +ATTR_EVENT_DATA = 'event_data' +ATTR_EVENT_TYPE = 'event_type' + +ATTR_TEMPLATE = 'template' +ATTR_TEMPLATE_VARIABLES = 'variables' + +ATTR_WEBHOOK_DATA = 'data' +ATTR_WEBHOOK_ENCRYPTED = 'encrypted' +ATTR_WEBHOOK_ENCRYPTED_DATA = 'encrypted_data' +ATTR_WEBHOOK_TYPE = 'type' + +WEBHOOK_TYPE_CALL_SERVICE = 'call_service' +WEBHOOK_TYPE_FIRE_EVENT = 'fire_event' +WEBHOOK_TYPE_RENDER_TEMPLATE = 'render_template' +WEBHOOK_TYPE_UPDATE_LOCATION = 'update_location' +WEBHOOK_TYPE_UPDATE_REGISTRATION = 'update_registration' + +WEBHOOK_TYPES = [WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, + WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, + WEBHOOK_TYPE_UPDATE_REGISTRATION] + +REGISTER_DEVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_APP_DATA, default={}): dict, + vol.Required(ATTR_APP_ID): cv.string, + vol.Optional(ATTR_APP_NAME): cv.string, + vol.Required(ATTR_APP_VERSION): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_MANUFACTURER): cv.string, + vol.Required(ATTR_MODEL): cv.string, + vol.Optional(ATTR_OS_VERSION): cv.string, + vol.Required(ATTR_SUPPORTS_ENCRYPTION, default=False): cv.boolean, +}) + +UPDATE_DEVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_APP_DATA, default={}): dict, + vol.Required(ATTR_APP_VERSION): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_MANUFACTURER): cv.string, + vol.Required(ATTR_MODEL): cv.string, + vol.Optional(ATTR_OS_VERSION): cv.string, +}) + +WEBHOOK_PAYLOAD_SCHEMA = vol.Schema({ + vol.Required(ATTR_WEBHOOK_TYPE): vol.In(WEBHOOK_TYPES), + vol.Required(ATTR_WEBHOOK_DATA, default={}): dict, + vol.Optional(ATTR_WEBHOOK_ENCRYPTED, default=False): cv.boolean, + vol.Optional(ATTR_WEBHOOK_ENCRYPTED_DATA): cv.string, +}) + +CALL_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_DOMAIN): cv.string, + vol.Required(ATTR_SERVICE): cv.string, + vol.Optional(ATTR_SERVICE_DATA, default={}): dict, +}) + +FIRE_EVENT_SCHEMA = vol.Schema({ + vol.Required(ATTR_EVENT_TYPE): cv.string, + vol.Optional(ATTR_EVENT_DATA, default={}): dict, +}) + +RENDER_TEMPLATE_SCHEMA = vol.Schema({ + vol.Required(ATTR_TEMPLATE): cv.string, + vol.Optional(ATTR_TEMPLATE_VARIABLES, default={}): dict, +}) + +WEBHOOK_SCHEMAS = { + WEBHOOK_TYPE_CALL_SERVICE: CALL_SERVICE_SCHEMA, + WEBHOOK_TYPE_FIRE_EVENT: FIRE_EVENT_SCHEMA, + WEBHOOK_TYPE_RENDER_TEMPLATE: RENDER_TEMPLATE_SCHEMA, + WEBHOOK_TYPE_UPDATE_LOCATION: SEE_SCHEMA, + WEBHOOK_TYPE_UPDATE_REGISTRATION: UPDATE_DEVICE_SCHEMA, +} + + +def get_cipher(): + """Return decryption function and length of key. + + Async friendly. + """ + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder + + def decrypt(ciphertext, key): + """Decrypt ciphertext using key.""" + return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder) + return (SecretBox.KEY_SIZE, decrypt) + + +def _decrypt_payload(key, ciphertext): + """Decrypt encrypted payload.""" + try: + keylen, decrypt = get_cipher() + except OSError: + _LOGGER.warning( + "Ignoring encrypted payload because libsodium not installed") + return None + + if key is None: + _LOGGER.warning( + "Ignoring encrypted payload because no decryption key known") + return None + + key = key.encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b'\0') + + try: + message = decrypt(ciphertext, key) + message = json.loads(message.decode("utf-8")) + _LOGGER.debug("Successfully decrypted mobile_app payload") + return message + except ValueError: + _LOGGER.warning("Ignoring encrypted payload because unable to decrypt") + return None + + +def context(device): + """Generate a context from a request.""" + return Context(user_id=device[CONF_USER_ID]) + + +async def handle_webhook(store, hass: HomeAssistantType, webhook_id: str, + request): + """Handle webhook callback.""" + device = hass.data[DOMAIN][webhook_id] + + try: + req_data = await request.json() + except ValueError: + _LOGGER.warning('Received invalid JSON from mobile_app') + return json_response([], status=HTTP_BAD_REQUEST) + + try: + req_data = WEBHOOK_PAYLOAD_SCHEMA(req_data) + except vol.Invalid as ex: + err = vol.humanize.humanize_error(req_data, ex) + _LOGGER.error('Received invalid webhook payload: %s', err) + return Response(status=200) + + webhook_type = req_data[ATTR_WEBHOOK_TYPE] + + webhook_payload = req_data.get(ATTR_WEBHOOK_DATA, {}) + + if req_data[ATTR_WEBHOOK_ENCRYPTED]: + enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA] + webhook_payload = _decrypt_payload(device[CONF_SECRET], enc_data) + + try: + data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload) + except vol.Invalid as ex: + err = vol.humanize.humanize_error(webhook_payload, ex) + _LOGGER.error('Received invalid webhook payload: %s', err) + return Response(status=200) + + if webhook_type == WEBHOOK_TYPE_CALL_SERVICE: + try: + await hass.services.async_call(data[ATTR_DOMAIN], + data[ATTR_SERVICE], + data[ATTR_SERVICE_DATA], + blocking=True, + context=context(device)) + except (vol.Invalid, ServiceNotFound): + raise HTTPBadRequest() + + return Response(status=200) + + if webhook_type == WEBHOOK_TYPE_FIRE_EVENT: + event_type = data[ATTR_EVENT_TYPE] + hass.bus.async_fire(event_type, data[ATTR_EVENT_DATA], + ha.EventOrigin.remote, context=context(device)) + return Response(status=200) + + if webhook_type == WEBHOOK_TYPE_RENDER_TEMPLATE: + try: + tpl = template.Template(data[ATTR_TEMPLATE], hass) + rendered = tpl.async_render(data.get(ATTR_TEMPLATE_VARIABLES)) + return json_response({"rendered": rendered}) + except (ValueError, TemplateError) as ex: + return json_response(({"error": ex}), status=HTTP_BAD_REQUEST) + + if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION: + await hass.services.async_call(DEVICE_TRACKER_DOMAIN, + DEVICE_TRACKER_SEE, data, + blocking=True, context=context(device)) + return Response(status=200) + + if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION: + data[ATTR_APP_ID] = device[ATTR_APP_ID] + data[ATTR_APP_NAME] = device[ATTR_APP_NAME] + data[ATTR_SUPPORTS_ENCRYPTION] = device[ATTR_SUPPORTS_ENCRYPTION] + data[CONF_SECRET] = device[CONF_SECRET] + data[CONF_USER_ID] = device[CONF_USER_ID] + data[CONF_WEBHOOK_ID] = device[CONF_WEBHOOK_ID] + + hass.data[DOMAIN][webhook_id] = data + + try: + await store.async_save(hass.data[DOMAIN]) + except HomeAssistantError as ex: + _LOGGER.error("Error updating mobile_app registration: %s", ex) + return Response(status=200) + + return json_response(safe_device(data)) + + +def supports_encryption(): + """Test if we support encryption.""" + try: + import nacl # noqa pylint: disable=unused-import + return True + except OSError: + return False + + +def safe_device(device: dict): + """Return a device without webhook_id or secret.""" + return { + ATTR_APP_DATA: device[ATTR_APP_DATA], + ATTR_APP_ID: device[ATTR_APP_ID], + ATTR_APP_NAME: device[ATTR_APP_NAME], + ATTR_APP_VERSION: device[ATTR_APP_VERSION], + ATTR_DEVICE_NAME: device[ATTR_DEVICE_NAME], + ATTR_MANUFACTURER: device[ATTR_MANUFACTURER], + ATTR_MODEL: device[ATTR_MODEL], + ATTR_OS_VERSION: device[ATTR_OS_VERSION], + ATTR_SUPPORTS_ENCRYPTION: device[ATTR_SUPPORTS_ENCRYPTION], + } + + +def register_device_webhook(hass: HomeAssistantType, store, device): + """Register the webhook for a device.""" + device_name = 'Mobile App: {}'.format(device[ATTR_DEVICE_NAME]) + webhook_id = device[CONF_WEBHOOK_ID] + webhook.async_register(hass, DOMAIN, device_name, webhook_id, + partial(handle_webhook, store)) + + +async def async_setup(hass, config): + """Set up the mobile app component.""" + conf = config.get(DOMAIN) + + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + app_config = await store.async_load() + if app_config is None: + app_config = {} + + hass.data[DOMAIN] = app_config + + for device in app_config.values(): + register_device_webhook(hass, store, device) + + if conf is not None: + hass.async_create_task(hass.config_entries.flow.async_init( + DOMAIN, context={'source': config_entries.SOURCE_IMPORT})) + + hass.http.register_view(DevicesView(store)) + + return True + + +async def async_setup_entry(hass, entry): + """Set up an mobile_app entry.""" + return True + + +class DevicesView(HomeAssistantView): + """A view that accepts device registration requests.""" + + url = '/api/mobile_app/devices' + name = 'api:mobile_app:register-device' + + def __init__(self, store): + """Initialize the view.""" + self._store = store + + @RequestDataValidator(REGISTER_DEVICE_SCHEMA) + async def post(self, request, data): + """Handle the POST request for device registration.""" + hass = request.app['hass'] + + resp = {} + + webhook_id = generate_secret() + + data[CONF_WEBHOOK_ID] = resp[CONF_WEBHOOK_ID] = webhook_id + + if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption(): + secret = generate_secret(16) + + data[CONF_SECRET] = resp[CONF_SECRET] = secret + + data[CONF_USER_ID] = request['hass_user'].id + + hass.data[DOMAIN][webhook_id] = data + + try: + await self._store.async_save(hass.data[DOMAIN]) + except HomeAssistantError: + return self.json_message("Error saving device.", + HTTP_INTERNAL_SERVER_ERROR) + + register_device_webhook(hass, self._store, data) + + return self.json(resp, status_code=HTTP_CREATED) diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index cc918dcf674..c0d3d152270 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -16,7 +16,7 @@ from homeassistant.setup import async_when_setup from .config_flow import CONF_SECRET -REQUIREMENTS = ['libnacl==1.6.1'] +REQUIREMENTS = ['PyNaCl==1.3.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 6818efbbf75..59e8c4825df 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -9,7 +9,7 @@ CONF_SECRET = 'secret' def supports_encryption(): """Test if we support encryption.""" try: - import libnacl # noqa pylint: disable=unused-import + import nacl # noqa pylint: disable=unused-import return True except OSError: return False diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index e85ebbe6fe1..be8698a47b1 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -4,7 +4,6 @@ Device tracker platform that adds support for OwnTracks over MQTT. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.owntracks/ """ -import base64 import json import logging @@ -37,13 +36,13 @@ def get_cipher(): Async friendly. """ - from libnacl import crypto_secretbox_KEYBYTES as KEYLEN - from libnacl.secret import SecretBox + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder def decrypt(ciphertext, key): """Decrypt ciphertext using key.""" - return SecretBox(key).decrypt(ciphertext) - return (KEYLEN, decrypt) + return SecretBox(key).decrypt(ciphertext, encoder=Base64Encoder) + return (SecretBox.KEY_SIZE, decrypt) def _parse_topic(topic, subscribe_topic): @@ -141,7 +140,6 @@ def _decrypt_payload(secret, topic, ciphertext): key = key.ljust(keylen, b'\0') try: - ciphertext = base64.b64decode(ciphertext) message = decrypt(ciphertext, key) message = message.decode("utf-8") _LOGGER.debug("Decrypted payload: %s", message) diff --git a/requirements_all.txt b/requirements_all.txt index 3ede6e21e36..79e68a873ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -50,6 +50,10 @@ PyMVGLive==1.1.4 # homeassistant.components.arduino PyMata==2.14 +# homeassistant.components.mobile_app +# homeassistant.components.owntracks +PyNaCl==1.3.0 + # homeassistant.auth.mfa_modules.totp PyQRCode==1.2.1 @@ -608,9 +612,6 @@ konnected==0.1.4 # homeassistant.components.eufy lakeside==0.12 -# homeassistant.components.owntracks -libnacl==1.6.1 - # homeassistant.components.dyson libpurecoollink==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2892b759c99..0840ee8f710 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -21,6 +21,10 @@ requests_mock==1.5.2 # homeassistant.components.homekit HAP-python==2.4.2 +# homeassistant.components.mobile_app +# homeassistant.components.owntracks +PyNaCl==1.3.0 + # homeassistant.components.sensor.rmvtransport PyRMVtransport==0.1.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 09eb9f21d4a..7db76b1361b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -108,6 +108,7 @@ TEST_REQUIREMENTS = ( 'pyupnp-async', 'pywebpush', 'pyHS100', + 'PyNaCl', 'regenmaschine', 'restrictedpython', 'rflink', diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py index 1ac3fc4a194..8e868296703 100644 --- a/tests/components/device_tracker/test_owntracks.py +++ b/tests/components/device_tracker/test_owntracks.py @@ -1295,18 +1295,25 @@ async def test_unsupported_message(hass, context): def generate_ciphers(secret): """Generate test ciphers for the DEFAULT_LOCATION_MESSAGE.""" - # libnacl ciphertext generation will fail if the module + # PyNaCl ciphertext generation will fail if the module # cannot be imported. However, the test for decryption # also relies on this library and won't be run without it. import pickle import base64 try: - from libnacl import crypto_secretbox_KEYBYTES as KEYLEN - from libnacl.secret import SecretBox - key = secret.encode("utf-8")[:KEYLEN].ljust(KEYLEN, b'\0') - ctxt = base64.b64encode(SecretBox(key).encrypt(json.dumps( - DEFAULT_LOCATION_MESSAGE).encode("utf-8"))).decode("utf-8") + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder + + keylen = SecretBox.KEY_SIZE + key = secret.encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b'\0') + + msg = json.dumps(DEFAULT_LOCATION_MESSAGE).encode("utf-8") + + ctxt = SecretBox(key).encrypt(msg, + encoder=Base64Encoder).decode("utf-8") except (ImportError, OSError): ctxt = '' @@ -1341,7 +1348,8 @@ def mock_cipher(): def mock_decrypt(ciphertext, key): """Decrypt/unpickle.""" import pickle - (mkey, plaintext) = pickle.loads(ciphertext) + import base64 + (mkey, plaintext) = pickle.loads(base64.b64decode(ciphertext)) if key != mkey: raise ValueError() return plaintext @@ -1443,9 +1451,9 @@ async def test_encrypted_payload_libsodium(hass, setup_comp): """Test sending encrypted message payload.""" try: # pylint: disable=unused-import - import libnacl # noqa: F401 + import nacl # noqa: F401 except (ImportError, OSError): - pytest.skip("libnacl/libsodium is not installed") + pytest.skip("PyNaCl/libsodium is not installed") return await setup_owntracks(hass, { diff --git a/tests/components/mobile_app/__init__.py b/tests/components/mobile_app/__init__.py new file mode 100644 index 00000000000..becdc2841f3 --- /dev/null +++ b/tests/components/mobile_app/__init__.py @@ -0,0 +1 @@ +"""Tests for mobile_app component.""" diff --git a/tests/components/mobile_app/test_init.py b/tests/components/mobile_app/test_init.py new file mode 100644 index 00000000000..d0c1ae02c6c --- /dev/null +++ b/tests/components/mobile_app/test_init.py @@ -0,0 +1,275 @@ +"""Test the mobile_app_http platform.""" +import pytest + +from homeassistant.setup import async_setup_component + +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.components.mobile_app import (DOMAIN, STORAGE_KEY, + STORAGE_VERSION, + CONF_SECRET, CONF_USER_ID) +from homeassistant.core import callback + +from tests.common import async_mock_service + +FIRE_EVENT = { + 'type': 'fire_event', + 'data': { + 'event_type': 'test_event', + 'event_data': { + 'hello': 'yo world' + } + } +} + +RENDER_TEMPLATE = { + 'type': 'render_template', + 'data': { + 'template': 'Hello world' + } +} + +CALL_SERVICE = { + 'type': 'call_service', + 'data': { + 'domain': 'test', + 'service': 'mobile_app', + 'service_data': { + 'foo': 'bar' + } + } +} + +REGISTER = { + 'app_data': {'foo': 'bar'}, + 'app_id': 'io.homeassistant.mobile_app_test', + 'app_name': 'Mobile App Tests', + 'app_version': '1.0.0', + 'device_name': 'Test 1', + 'manufacturer': 'mobile_app', + 'model': 'Test', + 'os_version': '1.0', + 'supports_encryption': True +} + +UPDATE = { + 'app_data': {'foo': 'bar'}, + 'app_version': '2.0.0', + 'device_name': 'Test 1', + 'manufacturer': 'mobile_app', + 'model': 'Test', + 'os_version': '1.0' +} + +# pylint: disable=redefined-outer-name + + +@pytest.fixture +def mobile_app_client(hass, aiohttp_client, hass_storage, hass_admin_user): + """mobile_app mock client.""" + hass_storage[STORAGE_KEY] = { + 'version': STORAGE_VERSION, + 'data': { + 'mobile_app_test': { + CONF_SECRET: '58eb127991594dad934d1584bdee5f27', + 'supports_encryption': True, + CONF_WEBHOOK_ID: 'mobile_app_test', + 'device_name': 'Test Device', + CONF_USER_ID: hass_admin_user.id, + } + } + } + + assert hass.loop.run_until_complete(async_setup_component( + hass, DOMAIN, { + DOMAIN: {} + })) + + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + + +@pytest.fixture +async def mock_api_client(hass, hass_client): + """Provide an authenticated client for mobile_app to use.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + return await hass_client() + + +async def test_handle_render_template(mobile_app_client): + """Test that we render templates properly.""" + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + json=RENDER_TEMPLATE + ) + + assert resp.status == 200 + + json = await resp.json() + assert json == {'rendered': 'Hello world'} + + +async def test_handle_call_services(hass, mobile_app_client): + """Test that we call services properly.""" + calls = async_mock_service(hass, 'test', 'mobile_app') + + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + json=CALL_SERVICE + ) + + assert resp.status == 200 + + assert len(calls) == 1 + + +async def test_handle_fire_event(hass, mobile_app_client): + """Test that we can fire events.""" + events = [] + + @callback + def store_event(event): + """Helepr to store events.""" + events.append(event) + + hass.bus.async_listen('test_event', store_event) + + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + json=FIRE_EVENT + ) + + assert resp.status == 200 + text = await resp.text() + assert text == "" + + assert len(events) == 1 + assert events[0].data['hello'] == 'yo world' + + +async def test_update_registration(mobile_app_client, hass_client): + """Test that a we can update an existing registration via webhook.""" + mock_api_client = await hass_client() + register_resp = await mock_api_client.post( + '/api/mobile_app/devices', json=REGISTER + ) + + assert register_resp.status == 201 + register_json = await register_resp.json() + + webhook_id = register_json[CONF_WEBHOOK_ID] + + update_container = { + 'type': 'update_registration', + 'data': UPDATE + } + + update_resp = await mobile_app_client.post( + '/api/webhook/{}'.format(webhook_id), json=update_container + ) + + assert update_resp.status == 200 + update_json = await update_resp.json() + assert update_json['app_version'] == '2.0.0' + assert CONF_WEBHOOK_ID not in update_json + assert CONF_SECRET not in update_json + + +async def test_returns_error_incorrect_json(mobile_app_client, caplog): + """Test that an error is returned when JSON is invalid.""" + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + data='not json' + ) + + assert resp.status == 400 + json = await resp.json() + assert json == [] + assert 'invalid JSON' in caplog.text + + +async def test_handle_decryption(mobile_app_client): + """Test that we can encrypt/decrypt properly.""" + try: + # pylint: disable=unused-import + from nacl.secret import SecretBox # noqa: F401 + from nacl.encoding import Base64Encoder # noqa: F401 + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + + keylen = SecretBox.KEY_SIZE + key = "58eb127991594dad934d1584bdee5f27".encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b'\0') + + payload = json.dumps({'template': 'Hello world'}).encode("utf-8") + + data = SecretBox(key).encrypt(payload, + encoder=Base64Encoder).decode("utf-8") + + container = { + 'type': 'render_template', + 'encrypted': True, + 'encrypted_data': data, + } + + resp = await mobile_app_client.post( + '/api/webhook/mobile_app_test', + json=container + ) + + assert resp.status == 200 + + json = await resp.json() + assert json == {'rendered': 'Hello world'} + + +async def test_register_device(hass_client, mock_api_client): + """Test that a device can be registered.""" + try: + # pylint: disable=unused-import + from nacl.secret import SecretBox # noqa: F401 + from nacl.encoding import Base64Encoder # noqa: F401 + except (ImportError, OSError): + pytest.skip("libnacl/libsodium is not installed") + return + + import json + + resp = await mock_api_client.post( + '/api/mobile_app/devices', json=REGISTER + ) + + assert resp.status == 201 + register_json = await resp.json() + assert CONF_WEBHOOK_ID in register_json + assert CONF_SECRET in register_json + + keylen = SecretBox.KEY_SIZE + key = register_json[CONF_SECRET].encode("utf-8") + key = key[:keylen] + key = key.ljust(keylen, b'\0') + + payload = json.dumps({'template': 'Hello world'}).encode("utf-8") + + data = SecretBox(key).encrypt(payload, + encoder=Base64Encoder).decode("utf-8") + + container = { + 'type': 'render_template', + 'encrypted': True, + 'encrypted_data': data, + } + + mobile_app_client = await hass_client() + + resp = await mobile_app_client.post( + '/api/webhook/{}'.format(register_json[CONF_WEBHOOK_ID]), + json=container + ) + + assert resp.status == 200 + + webhook_json = await resp.json() + assert webhook_json == {'rendered': 'Hello world'} From e877983533c8c6ed9ae212e35e92a80d5e07dc22 Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sat, 2 Mar 2019 08:09:12 +0100 Subject: [PATCH 230/253] Make time trigger data trigger.now local (#21544) * Make time trigger data trigger.now local * Make time pattern trigger data trigger.now local * Lint * Rework according to review comment * Lint --- homeassistant/helpers/event.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index c1dae00bed5..b55c259f503 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -370,7 +370,9 @@ def async_track_utc_time_change(hass, action, last_now = now if next_time <= now: - hass.async_run_job(action, event.data[ATTR_NOW]) + if local: + now = dt_util.as_local(now) + hass.async_run_job(action, now) calculate_next(now + timedelta(seconds=1)) # We can't use async_track_point_in_utc_time here because it would From 996e0a6389199e0122ce41eecdae3e885b4d3fd8 Mon Sep 17 00:00:00 2001 From: damarco Date: Fri, 1 Mar 2019 19:47:20 +0100 Subject: [PATCH 231/253] Bump zigpy-deconz (#21566) --- homeassistant/components/zha/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 96c3a30d313..cafbae13421 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -33,7 +33,7 @@ REQUIREMENTS = [ 'zigpy-homeassistant==0.3.0', 'zigpy-xbee-homeassistant==0.1.2', 'zha-quirks==0.0.6', - 'zigpy-deconz==0.1.1' + 'zigpy-deconz==0.1.2' ] DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ diff --git a/requirements_all.txt b/requirements_all.txt index 79e68a873ee..be4570b5457 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1823,7 +1823,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.1.1 +zigpy-deconz==0.1.2 # homeassistant.components.zha zigpy-homeassistant==0.3.0 From a268aab2ec4c29abeec82f5e5a4fc18ef419e6f3 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 1 Mar 2019 23:09:31 -0800 Subject: [PATCH 232/253] Re-thrown exception occurred in the blocking service call (#21573) * Rethrown exception occurred in the actual service call * Fix lint and test --- .../components/websocket_api/commands.py | 9 +++- .../components/websocket_api/const.py | 1 + homeassistant/helpers/service.py | 10 +++- tests/components/deconz/test_climate.py | 14 ++++-- .../components/websocket_api/test_commands.py | 46 +++++++++++++++++++ tests/test_core.py | 42 ++++++++++++++++- 6 files changed, 114 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 34bb04cb394..33a41dc8511 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -3,7 +3,8 @@ import voluptuous as vol from homeassistant.const import MATCH_ALL, EVENT_TIME_CHANGED from homeassistant.core import callback, DOMAIN as HASS_DOMAIN -from homeassistant.exceptions import Unauthorized, ServiceNotFound +from homeassistant.exceptions import Unauthorized, ServiceNotFound, \ + HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_get_all_descriptions @@ -149,6 +150,12 @@ async def handle_call_service(hass, connection, msg): except ServiceNotFound: connection.send_message(messages.error_message( msg['id'], const.ERR_NOT_FOUND, 'Service not found.')) + except HomeAssistantError as err: + connection.send_message(messages.error_message( + msg['id'], const.ERR_HOME_ASSISTANT_ERROR, '{}'.format(err))) + except Exception as err: # pylint: disable=broad-except + connection.send_message(messages.error_message( + msg['id'], const.ERR_UNKNOWN_ERROR, '{}'.format(err))) @callback diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index fd8f7eb7b08..01145275b31 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -9,6 +9,7 @@ MAX_PENDING_MSG = 512 ERR_ID_REUSE = 'id_reuse' ERR_INVALID_FORMAT = 'invalid_format' ERR_NOT_FOUND = 'not_found' +ERR_HOME_ASSISTANT_ERROR = 'home_assistant_error' ERR_UNKNOWN_COMMAND = 'unknown_command' ERR_UNKNOWN_ERROR = 'unknown_error' ERR_UNAUTHORIZED = 'unauthorized' diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index d2211d031f5..22138d7c2aa 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -272,7 +272,10 @@ async def entity_service_call(hass, platforms, func, call, service_name=''): ] if tasks: - await asyncio.wait(tasks) + done, pending = await asyncio.wait(tasks) + assert not pending + for future in done: + future.result() # pop exception if have async def _handle_service_platform_call(func, data, entities, context): @@ -294,4 +297,7 @@ async def _handle_service_platform_call(func, data, entities, context): tasks.append(entity.async_update_ha_state(True)) if tasks: - await asyncio.wait(tasks) + done, pending = await asyncio.wait(tasks) + assert not pending + for future in done: + future.result() # pop exception if have diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 13083594c8a..fa274f1d676 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -1,6 +1,8 @@ """deCONZ climate platform tests.""" from unittest.mock import Mock, patch +import asynctest + from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -43,8 +45,14 @@ ENTRY_CONFIG = { async def setup_gateway(hass, data, allow_clip_sensor=True): """Load the deCONZ sensor platform.""" from pydeconz import DeconzSession - loop = Mock() - session = Mock() + + session = Mock(put=asynctest.CoroutineMock( + return_value=Mock(status=200, + json=asynctest.CoroutineMock(), + text=asynctest.CoroutineMock(), + ) + ) + ) ENTRY_CONFIG[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor @@ -52,7 +60,7 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): 1, deconz.DOMAIN, 'Mock Title', ENTRY_CONFIG, 'test', config_entries.CONN_CLASS_LOCAL_PUSH) gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(loop, session, **config_entry.data) + gateway.api = DeconzSession(hass.loop, session, **config_entry.data) gateway.api.config = Mock() hass.data[deconz.DOMAIN] = gateway diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 78a5bf6d57e..c9ec04c5d7e 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -7,6 +7,7 @@ from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED ) from homeassistant.components.websocket_api import const, commands +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -66,6 +67,51 @@ async def test_call_service_not_found(hass, websocket_client): assert msg['error']['code'] == const.ERR_NOT_FOUND +async def test_call_service_error(hass, websocket_client): + """Test call service command with error.""" + @callback + def ha_error_call(_): + raise HomeAssistantError('error_message') + + hass.services.async_register('domain_test', 'ha_error', ha_error_call) + + async def unknown_error_call(_): + raise ValueError('value_error') + + hass.services.async_register( + 'domain_test', 'unknown_error', unknown_error_call) + + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'ha_error', + }) + + msg = await websocket_client.receive_json() + print(msg) + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'home_assistant_error' + assert msg['error']['message'] == 'error_message' + + await websocket_client.send_json({ + 'id': 6, + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'unknown_error', + }) + + msg = await websocket_client.receive_json() + print(msg) + assert msg['id'] == 6 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'unknown_error' + assert msg['error']['message'] == 'value_error' + + async def test_subscribe_unsubscribe_events(hass, websocket_client): """Test subscribe/unsubscribe events command.""" init_count = sum(hass.bus.async_listeners().values()) diff --git a/tests/test_core.py b/tests/test_core.py index e2ed249f441..ef9621bdac7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -726,8 +726,7 @@ class TestServiceRegistry(unittest.TestCase): """Test registering and calling an async service.""" calls = [] - @asyncio.coroutine - def service_handler(call): + async def service_handler(call): """Service handler coroutine.""" calls.append(call) @@ -803,6 +802,45 @@ class TestServiceRegistry(unittest.TestCase): self.hass.block_till_done() assert len(calls_remove) == 0 + def test_async_service_raise_exception(self): + """Test registering and calling an async service raise exception.""" + async def service_handler(_): + """Service handler coroutine.""" + raise ValueError + + self.services.register( + 'test_domain', 'register_calls', service_handler) + self.hass.block_till_done() + + with pytest.raises(ValueError): + assert self.services.call('test_domain', 'REGISTER_CALLS', + blocking=True) + self.hass.block_till_done() + + # Non-blocking service call never throw exception + self.services.call('test_domain', 'REGISTER_CALLS', blocking=False) + self.hass.block_till_done() + + def test_callback_service_raise_exception(self): + """Test registering and calling an callback service raise exception.""" + @ha.callback + def service_handler(_): + """Service handler coroutine.""" + raise ValueError + + self.services.register( + 'test_domain', 'register_calls', service_handler) + self.hass.block_till_done() + + with pytest.raises(ValueError): + assert self.services.call('test_domain', 'REGISTER_CALLS', + blocking=True) + self.hass.block_till_done() + + # Non-blocking service call never throw exception + self.services.call('test_domain', 'REGISTER_CALLS', blocking=False) + self.hass.block_till_done() + class TestConfig(unittest.TestCase): """Test configuration methods.""" From ec9ccf640254e21558af750fd06903c84815e983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 2 Mar 2019 11:27:36 +0100 Subject: [PATCH 233/253] Upgrade PyXiaomiGateway library (#21582) --- homeassistant/components/xiaomi_aqara/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 5e47adc47f9..19d7aaaa30d 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['PyXiaomiGateway==0.11.2'] +REQUIREMENTS = ['PyXiaomiGateway==0.12.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index be4570b5457..2ae1e667cf6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -67,7 +67,7 @@ PyRMVtransport==0.1.3 PyTransportNSW==0.1.1 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.11.2 +PyXiaomiGateway==0.12.0 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.5 From c5dad8221177a689bc8d5b0d8884a0a7766fbae0 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sun, 3 Mar 2019 21:22:22 -0800 Subject: [PATCH 234/253] Log exception occurred in WS service call command (#21584) --- homeassistant/components/websocket_api/commands.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 33a41dc8511..3313971e79e 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -151,9 +151,11 @@ async def handle_call_service(hass, connection, msg): connection.send_message(messages.error_message( msg['id'], const.ERR_NOT_FOUND, 'Service not found.')) except HomeAssistantError as err: + connection.logger.exception(err) connection.send_message(messages.error_message( msg['id'], const.ERR_HOME_ASSISTANT_ERROR, '{}'.format(err))) except Exception as err: # pylint: disable=broad-except + connection.logger.exception(err) connection.send_message(messages.error_message( msg['id'], const.ERR_UNKNOWN_ERROR, '{}'.format(err))) From e6debe09e80372a2754f044a26f95c1ccf5e371b Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 2 Mar 2019 12:32:18 +0100 Subject: [PATCH 235/253] Word the tplink deprecation warning more strongly (#21586) --- homeassistant/components/tplink/light.py | 2 +- homeassistant/components/tplink/switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 1e31df98af5..de1a943c33a 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -34,7 +34,7 @@ def async_setup_platform(hass, config, add_entities, discovery_info=None): Deprecated. """ - _LOGGER.warning('Loading as a platform is deprecated, ' + _LOGGER.warning('Loading as a platform is no longer supported, ' 'convert to use the tplink component.') diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index efff0eb4f51..65b884169c7 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -29,7 +29,7 @@ def async_setup_platform(hass, config, add_entities, discovery_info=None): Deprecated. """ - _LOGGER.warning('Loading as a platform is deprecated, ' + _LOGGER.warning('Loading as a platform is no longer supported, ' 'convert to use the tplink component.') From d9806f759bc6836fcebca45eb4a9d453ffaf921a Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sat, 2 Mar 2019 21:57:57 -0600 Subject: [PATCH 236/253] Handle when installed app has already been removed (#21595) --- homeassistant/components/smartthings/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index e64988b2697..64e717cbc92 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -48,10 +48,20 @@ async def async_migrate_entry(hass: HomeAssistantType, entry: ConfigEntry): """ from pysmartthings import SmartThings - # Delete the installed app + # Remove the installed_app, which if already removed raises a 403 error. api = SmartThings(async_get_clientsession(hass), entry.data[CONF_ACCESS_TOKEN]) - await api.delete_installed_app(entry.data[CONF_INSTALLED_APP_ID]) + installed_app_id = entry.data[CONF_INSTALLED_APP_ID] + try: + await api.delete_installed_app(installed_app_id) + except ClientResponseError as ex: + if ex.status == 403: + _LOGGER.exception("Installed app %s has already been removed", + installed_app_id) + else: + raise + _LOGGER.debug("Removed installed app %s", installed_app_id) + # Delete the entry hass.async_create_task( hass.config_entries.async_remove(entry.entry_id)) From d5bdfdb0b3b1e580c45fa3cb99ef9c83d17e4da2 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Mon, 4 Mar 2019 15:55:26 -0800 Subject: [PATCH 237/253] Resolve race condition when HA auth provider is loading (#21619) * Resolve race condition when HA auth provider is loading * Fix * Add more tests * Lint --- homeassistant/auth/mfa_modules/notify.py | 20 +++++++++----- homeassistant/auth/mfa_modules/totp.py | 14 +++++++--- homeassistant/auth/providers/homeassistant.py | 17 ++++++++---- tests/auth/mfa_modules/test_notify.py | 24 +++++++++++++++++ tests/auth/mfa_modules/test_totp.py | 24 +++++++++++++++++ tests/auth/providers/test_homeassistant.py | 27 +++++++++++++++++++ 6 files changed, 110 insertions(+), 16 deletions(-) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 3c26f8b4bde..310abff9484 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -2,6 +2,7 @@ Sending HOTP through notify service """ +import asyncio import logging from collections import OrderedDict from typing import Any, Dict, Optional, List @@ -90,6 +91,7 @@ class NotifyAuthModule(MultiFactorAuthModule): self._include = config.get(CONF_INCLUDE, []) self._exclude = config.get(CONF_EXCLUDE, []) self._message_template = config[CONF_MESSAGE] + self._init_lock = asyncio.Lock() @property def input_schema(self) -> vol.Schema: @@ -98,15 +100,19 @@ class NotifyAuthModule(MultiFactorAuthModule): async def _async_load(self) -> None: """Load stored data.""" - data = await self._user_store.async_load() + async with self._init_lock: + if self._user_settings is not None: + return - if data is None: - data = {STORAGE_USERS: {}} + data = await self._user_store.async_load() - self._user_settings = { - user_id: NotifySetting(**setting) - for user_id, setting in data.get(STORAGE_USERS, {}).items() - } + if data is None: + data = {STORAGE_USERS: {}} + + self._user_settings = { + user_id: NotifySetting(**setting) + for user_id, setting in data.get(STORAGE_USERS, {}).items() + } async def _async_save(self) -> None: """Save data.""" diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 68f4e1d0596..dc51152f565 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -1,4 +1,5 @@ """Time-based One Time Password auth module.""" +import asyncio import logging from io import BytesIO from typing import Any, Dict, Optional, Tuple # noqa: F401 @@ -68,6 +69,7 @@ class TotpAuthModule(MultiFactorAuthModule): self._users = None # type: Optional[Dict[str, str]] self._user_store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True) + self._init_lock = asyncio.Lock() @property def input_schema(self) -> vol.Schema: @@ -76,12 +78,16 @@ class TotpAuthModule(MultiFactorAuthModule): async def _async_load(self) -> None: """Load stored data.""" - data = await self._user_store.async_load() + async with self._init_lock: + if self._users is not None: + return - if data is None: - data = {STORAGE_USERS: {}} + data = await self._user_store.async_load() - self._users = data.get(STORAGE_USERS, {}) + if data is None: + data = {STORAGE_USERS: {}} + + self._users = data.get(STORAGE_USERS, {}) async def _async_save(self) -> None: """Save data.""" diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index b22f93f11f1..2187d272800 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -1,4 +1,5 @@ """Home Assistant auth provider.""" +import asyncio import base64 from collections import OrderedDict import logging @@ -204,15 +205,21 @@ class HassAuthProvider(AuthProvider): DEFAULT_TITLE = 'Home Assistant Local' - data = None + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize an Home Assistant auth provider.""" + super().__init__(*args, **kwargs) + self.data = None # type: Optional[Data] + self._init_lock = asyncio.Lock() async def async_initialize(self) -> None: """Initialize the auth provider.""" - if self.data is not None: - return + async with self._init_lock: + if self.data is not None: + return - self.data = Data(self.hass) - await self.data.async_load() + data = Data(self.hass) + await data.async_load() + self.data = data async def async_login_flow( self, context: Optional[Dict]) -> LoginFlow: diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index 748b5507824..c0680024dae 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -1,4 +1,5 @@ """Test the HMAC-based One Time Password (MFA) auth module.""" +import asyncio from unittest.mock import patch from homeassistant import data_entry_flow @@ -395,3 +396,26 @@ async def test_not_raise_exception_when_service_not_exist(hass): # wait service call finished await hass.async_block_till_done() + + +async def test_race_condition_in_data_loading(hass): + """Test race condition in the data loading.""" + counter = 0 + + async def mock_load(_): + """Mock homeassistant.helpers.storage.Store.async_load.""" + nonlocal counter + counter += 1 + await asyncio.sleep(0) + + notify_auth_module = await auth_mfa_module_from_config(hass, { + 'type': 'notify' + }) + with patch('homeassistant.helpers.storage.Store.async_load', + new=mock_load): + task1 = notify_auth_module.async_validate('user', {'code': 'value'}) + task2 = notify_auth_module.async_validate('user', {'code': 'value'}) + results = await asyncio.gather(task1, task2, return_exceptions=True) + assert counter == 1 + assert results[0] is False + assert results[1] is False diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index d400fe80672..35ab21ae6de 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -1,4 +1,5 @@ """Test the Time-based One Time Password (MFA) auth module.""" +import asyncio from unittest.mock import patch from homeassistant import data_entry_flow @@ -128,3 +129,26 @@ async def test_login_flow_validates_mfa(hass): result['flow_id'], {'code': MOCK_CODE}) assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['data'].id == 'mock-user' + + +async def test_race_condition_in_data_loading(hass): + """Test race condition in the data loading.""" + counter = 0 + + async def mock_load(_): + """Mock of homeassistant.helpers.storage.Store.async_load.""" + nonlocal counter + counter += 1 + await asyncio.sleep(0) + + totp_auth_module = await auth_mfa_module_from_config(hass, { + 'type': 'totp' + }) + with patch('homeassistant.helpers.storage.Store.async_load', + new=mock_load): + task1 = totp_auth_module.async_validate('user', {'code': 'value'}) + task2 = totp_auth_module.async_validate('user', {'code': 'value'}) + results = await asyncio.gather(task1, task2, return_exceptions=True) + assert counter == 1 + assert results[0] is False + assert results[1] is False diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index ffc4d67f21d..c466a1fa42b 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -1,4 +1,5 @@ """Test the Home Assistant local auth provider.""" +import asyncio from unittest.mock import Mock, patch import pytest @@ -288,3 +289,29 @@ async def test_legacy_get_or_create_credentials(hass, legacy_data): 'username': 'hello ' }) assert credentials1 is not credentials3 + + +async def test_race_condition_in_data_loading(hass): + """Test race condition in the hass_auth.Data loading. + + Ref issue: https://github.com/home-assistant/home-assistant/issues/21569 + """ + counter = 0 + + async def mock_load(_): + """Mock of homeassistant.helpers.storage.Store.async_load.""" + nonlocal counter + counter += 1 + await asyncio.sleep(0) + + provider = hass_auth.HassAuthProvider(hass, auth_store.AuthStore(hass), + {'type': 'homeassistant'}) + with patch('homeassistant.helpers.storage.Store.async_load', + new=mock_load): + task1 = provider.async_validate_login('user', 'pass') + task2 = provider.async_validate_login('user', 'pass') + results = await asyncio.gather(task1, task2, return_exceptions=True) + assert counter == 1 + assert isinstance(results[0], hass_auth.InvalidAuth) + # results[1] will be a TypeError if race condition occurred + assert isinstance(results[1], hass_auth.InvalidAuth) From 932080656df5ba9a6cfcfbae11809cc64d3f353c Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 3 Mar 2019 18:49:29 +0100 Subject: [PATCH 238/253] Upgrade pysonos to 0.0.8 (#21624) --- homeassistant/components/sonos/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index bcac4ce272c..e9f297e4f07 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -4,7 +4,7 @@ from homeassistant.helpers import config_entry_flow DOMAIN = 'sonos' -REQUIREMENTS = ['pysonos==0.0.7'] +REQUIREMENTS = ['pysonos==0.0.8'] async def async_setup(hass, config): diff --git a/requirements_all.txt b/requirements_all.txt index 2ae1e667cf6..3d9236fbd34 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1267,7 +1267,7 @@ pysmartthings==0.6.3 pysnmp==4.4.8 # homeassistant.components.sonos -pysonos==0.0.7 +pysonos==0.0.8 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0840ee8f710..6e5638d167a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -230,7 +230,7 @@ pysmartapp==0.3.0 pysmartthings==0.6.3 # homeassistant.components.sonos -pysonos==0.0.7 +pysonos==0.0.8 # homeassistant.components.spc pyspcwebgw==0.4.0 From cca8d4c9516d879df1a1e9c76ba3f85e72951ebd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Mar 2019 20:36:13 -0800 Subject: [PATCH 239/253] Fix calc next (#21630) --- homeassistant/helpers/event.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b55c259f503..5e262a47565 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -370,9 +370,7 @@ def async_track_utc_time_change(hass, action, last_now = now if next_time <= now: - if local: - now = dt_util.as_local(now) - hass.async_run_job(action, now) + hass.async_run_job(action, dt_util.as_local(now) if local else now) calculate_next(now + timedelta(seconds=1)) # We can't use async_track_point_in_utc_time here because it would From a382ba731d75f0aa3e3aa49976f0a3686d20e05b Mon Sep 17 00:00:00 2001 From: Gijs Reichert Date: Mon, 4 Mar 2019 15:25:28 +0100 Subject: [PATCH 240/253] Cast displaytime to int for JSON RPC (#21649) --- homeassistant/components/notify/kodi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/notify/kodi.py b/homeassistant/components/notify/kodi.py index 74bfe61d3f2..50d2246cd29 100644 --- a/homeassistant/components/notify/kodi.py +++ b/homeassistant/components/notify/kodi.py @@ -90,7 +90,7 @@ class KodiNotificationService(BaseNotificationService): try: data = kwargs.get(ATTR_DATA) or {} - displaytime = data.get(ATTR_DISPLAYTIME, 10000) + displaytime = int(data.get(ATTR_DISPLAYTIME, 10000)) icon = data.get(ATTR_ICON, "info") title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) await self._server.GUI.ShowNotification( From f5a0b5ab98a531c5f059ec3962be066cede256c1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Mar 2019 00:53:16 +0100 Subject: [PATCH 241/253] :shirt: Corrects unit of measurement symbol for Watt (#21654) --- homeassistant/components/toon/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/toon/const.py b/homeassistant/components/toon/const.py index 762374eb41c..29b58fbfff9 100644 --- a/homeassistant/components/toon/const.py +++ b/homeassistant/components/toon/const.py @@ -14,7 +14,7 @@ DEFAULT_MAX_TEMP = 30.0 DEFAULT_MIN_TEMP = 6.0 CURRENCY_EUR = 'EUR' -POWER_WATT = 'Watt' +POWER_WATT = 'W' POWER_KWH = 'kWh' RATIO_PERCENT = '%' VOLUME_CM3 = 'CM3' From 81c252f91745107ffcf9c96765dc8681d5fb1727 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Mar 2019 13:18:16 -0800 Subject: [PATCH 242/253] Rename Google Assistant evenets (#21655) --- homeassistant/components/google_assistant/const.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index b7d3a398ef2..220ed6dd58c 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -44,6 +44,6 @@ ERR_UNKNOWN_ERROR = 'unknownError' ERR_FUNCTION_NOT_SUPPORTED = 'functionNotSupported' # Event types -EVENT_COMMAND_RECEIVED = 'google_assistant_command_received' -EVENT_QUERY_RECEIVED = 'google_assistant_query_received' -EVENT_SYNC_RECEIVED = 'google_assistant_sync_received' +EVENT_COMMAND_RECEIVED = 'google_assistant_command' +EVENT_QUERY_RECEIVED = 'google_assistant_query' +EVENT_SYNC_RECEIVED = 'google_assistant_sync' From 31b88197eb65e5d406bd56c127907752d44ce2a8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Mar 2019 00:52:00 +0100 Subject: [PATCH 243/253] :ambulance: Fixes Toon doing I/O in coroutines (#21657) --- homeassistant/components/toon/__init__.py | 10 ++++++---- .../components/toon/binary_sensor.py | 2 +- homeassistant/components/toon/climate.py | 2 +- homeassistant/components/toon/config_flow.py | 20 +++++++++---------- homeassistant/components/toon/sensor.py | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index fce0bc4ed2a..00006fd7c17 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -1,6 +1,7 @@ """Support for Toon van Eneco devices.""" import logging from typing import Any, Dict +from functools import partial import voluptuous as vol @@ -48,10 +49,11 @@ async def async_setup_entry(hass: HomeAssistantType, conf = hass.data.get(DATA_TOON_CONFIG) - toon = Toon(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], - conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET], - tenant_id=entry.data[CONF_TENANT], - display_common_name=entry.data[CONF_DISPLAY]) + toon = await hass.async_add_executor_job(partial( + Toon, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], + conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET], + tenant_id=entry.data[CONF_TENANT], + display_common_name=entry.data[CONF_DISPLAY])) hass.data.setdefault(DATA_TOON_CLIENT, {})[entry.entry_id] = toon diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py index 891a72daeed..a50a67085ec 100644 --- a/homeassistant/components/toon/binary_sensor.py +++ b/homeassistant/components/toon/binary_sensor.py @@ -102,7 +102,7 @@ class ToonBinarySensor(ToonEntity, BinarySensorDevice): return value - async def async_update(self) -> None: + def update(self) -> None: """Get the latest data from the binary sensor.""" section = getattr(self.toon, self.section) self._state = getattr(section, self.measurement) diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index 2e564b8457a..13f1c1269a1 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -117,7 +117,7 @@ class ToonThermostatDevice(ToonDisplayDeviceEntity, ClimateDevice): """Set new operation mode.""" self.toon.thermostat_state = HA_TOON[operation_mode] - async def async_update(self) -> None: + def update(self) -> None: """Update local state.""" if self.toon.thermostat_state is None: self._state = None diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index cdb8a0f2257..a09b3dd49a7 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure the Toon component.""" from collections import OrderedDict import logging +from functools import partial import voluptuous as vol @@ -75,11 +76,10 @@ class ToonFlowHandler(config_entries.ConfigFlow): app = self.hass.data.get(DATA_TOON_CONFIG, {}) try: - toon = Toon(user_input[CONF_USERNAME], - user_input[CONF_PASSWORD], - app[CONF_CLIENT_ID], - app[CONF_CLIENT_SECRET], - tenant_id=user_input[CONF_TENANT]) + toon = await self.hass.async_add_executor_job(partial( + Toon, user_input[CONF_USERNAME], user_input[CONF_PASSWORD], + app[CONF_CLIENT_ID], app[CONF_CLIENT_SECRET], + tenant_id=user_input[CONF_TENANT])) displays = toon.display_names @@ -136,12 +136,10 @@ class ToonFlowHandler(config_entries.ConfigFlow): app = self.hass.data.get(DATA_TOON_CONFIG, {}) try: - Toon(self.username, - self.password, - app[CONF_CLIENT_ID], - app[CONF_CLIENT_SECRET], - tenant_id=self.tenant, - display_common_name=user_input[CONF_DISPLAY]) + await self.hass.async_add_executor_job(partial( + Toon, self.username, self.password, app[CONF_CLIENT_ID], + app[CONF_CLIENT_SECRET], tenant_id=self.tenant, + display_common_name=user_input[CONF_DISPLAY])) except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected error while authenticating") diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index 2a5921b78eb..e263bda9fc7 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -134,7 +134,7 @@ class ToonSensor(ToonEntity): """Return the unit this state is expressed in.""" return self._unit_of_measurement - async def async_update(self) -> None: + def update(self) -> None: """Get the latest data from the sensor.""" section = getattr(self.toon, self.section) value = None From a778cd117f6165837a59906803efd4fea96cc198 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Mar 2019 00:51:15 +0100 Subject: [PATCH 244/253] Upgrade toonapilib to 3.1.0 (#21661) --- homeassistant/components/toon/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 00006fd7c17..12cae9ac801 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -16,7 +16,7 @@ from .const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, DATA_TOON_CLIENT, DATA_TOON_CONFIG, DOMAIN) -REQUIREMENTS = ['toonapilib==3.0.9'] +REQUIREMENTS = ['toonapilib==3.1.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 3d9236fbd34..c3784acacf9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1689,7 +1689,7 @@ tikteck==0.4 todoist-python==7.0.17 # homeassistant.components.toon -toonapilib==3.0.9 +toonapilib==3.1.0 # homeassistant.components.alarm_control_panel.totalconnect total_connect_client==0.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e5638d167a..d404e18a06f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -295,7 +295,7 @@ srpenergy==1.0.5 statsd==3.2.1 # homeassistant.components.toon -toonapilib==3.0.9 +toonapilib==3.1.0 # homeassistant.components.camera.uvc uvcclient==0.11.0 From b20b811cb936d387518b1bdc611b1239be2c78a6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Mar 2019 16:01:31 -0800 Subject: [PATCH 245/253] Avoid recorder thread crashing (#21668) --- homeassistant/components/recorder/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index e0af36ea409..6c338457b34 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -318,6 +318,10 @@ class Recorder(threading.Thread): CONNECT_RETRY_WAIT) tries += 1 + except exc.SQLAlchemyError: + updated = True + _LOGGER.exception("Error saving event: %s", event) + if not updated: _LOGGER.error("Error in database update. Could not save " "after %d tries. Giving up", tries) From 3135257c0dc17e5eef2a2354d20ba07d365abec9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Mar 2019 16:02:05 -0800 Subject: [PATCH 246/253] Bumped version to 0.89.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a3dfff9a0b0..6e68c2f8e04 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 89 -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 2303e1684ef208bbacece90ebde01ea07d9d64a9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Mar 2019 11:45:31 -0800 Subject: [PATCH 247/253] Updated frontend to 20190305.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 fbbea13f026..d7c1aabdb49 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,7 +21,7 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage -REQUIREMENTS = ['home-assistant-frontend==20190303.0'] +REQUIREMENTS = ['home-assistant-frontend==20190305.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', diff --git a/requirements_all.txt b/requirements_all.txt index c3784acacf9..088800d3c4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -539,7 +539,7 @@ hole==0.3.0 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190303.0 +home-assistant-frontend==20190305.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d404e18a06f..154d6442d25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -120,7 +120,7 @@ hdate==0.8.7 holidays==0.9.9 # homeassistant.components.frontend -home-assistant-frontend==20190303.0 +home-assistant-frontend==20190305.0 # homeassistant.components.homekit_controller homekit==0.12.2 From 4978a1681ead511127e6170d26f0fbd002fd8fce Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 5 Mar 2019 05:18:25 +0000 Subject: [PATCH 248/253] check we have a tb (#21670) --- homeassistant/components/system_log/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 16786bdeba4..d6877c32f0d 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -91,15 +91,15 @@ class LogEntry: self.first_occured = self.timestamp = record.created self.level = record.levelname self.message = record.getMessage() + self.exception = '' + self.root_cause = None if record.exc_info: self.exception = ''.join( traceback.format_exception(*record.exc_info)) _, _, tb = record.exc_info # pylint: disable=invalid-name # Last line of traceback contains the root cause of the exception - self.root_cause = str(traceback.extract_tb(tb)[-1]) - else: - self.exception = '' - self.root_cause = None + if traceback.extract_tb(tb): + self.root_cause = str(traceback.extract_tb(tb)[-1]) self.source = source self.count = 1 From cb613984dfb5094412f39128e400da657701c9b8 Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Tue, 5 Mar 2019 11:07:40 +0100 Subject: [PATCH 249/253] Fix ADS race condition (#21677) --- homeassistant/components/ads/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 060e9b2b987..1b90e645af4 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -171,13 +171,12 @@ class AdsHub: hnotify, huser = self._client.add_device_notification( name, attr, self._device_notification_callback) hnotify = int(hnotify) + self._notification_items[hnotify] = NotificationItem( + hnotify, huser, name, plc_datatype, callback) _LOGGER.debug( "Added device notification %d for variable %s", hnotify, name) - self._notification_items[hnotify] = NotificationItem( - hnotify, huser, name, plc_datatype, callback) - def _device_notification_callback(self, notification, name): """Handle device notifications.""" contents = notification.contents @@ -187,9 +186,10 @@ class AdsHub: data = contents.data try: - notification_item = self._notification_items[hnotify] + with self._lock: + notification_item = self._notification_items[hnotify] except KeyError: - _LOGGER.debug("Unknown device notification handle: %d", hnotify) + _LOGGER.error("Unknown device notification handle: %d", hnotify) return # Parse data to desired datatype From 4c72f3c48b05c3a9a3e3f6fa575efeddbd1156b5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Mar 2019 11:46:30 -0800 Subject: [PATCH 250/253] Bumped version to 0.89.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6e68c2f8e04..31f7d11734c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 89 -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 c2f4293c6a45309eac2ccfcbdeb1126aa0c6e96a Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 6 Mar 2019 13:52:25 +0100 Subject: [PATCH 251/253] resync hass that changes have occured (#21705) --- homeassistant/components/tellduslive/cover.py | 3 +++ homeassistant/components/tellduslive/light.py | 1 + homeassistant/components/tellduslive/switch.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/homeassistant/components/tellduslive/cover.py b/homeassistant/components/tellduslive/cover.py index 5a22311d7f0..1bd3158d100 100644 --- a/homeassistant/components/tellduslive/cover.py +++ b/homeassistant/components/tellduslive/cover.py @@ -44,11 +44,14 @@ class TelldusLiveCover(TelldusLiveEntity, CoverDevice): def close_cover(self, **kwargs): """Close the cover.""" self.device.down() + self._update_callback() def open_cover(self, **kwargs): """Open the cover.""" self.device.up() + self._update_callback() def stop_cover(self, **kwargs): """Stop the cover.""" self.device.stop() + self._update_callback() diff --git a/homeassistant/components/tellduslive/light.py b/homeassistant/components/tellduslive/light.py index 10eaee1ad8b..12baf8384f6 100644 --- a/homeassistant/components/tellduslive/light.py +++ b/homeassistant/components/tellduslive/light.py @@ -45,6 +45,7 @@ class TelldusLiveLight(TelldusLiveEntity, Light): def changed(self): """Define a property of the device that might have changed.""" self._last_brightness = self.brightness + self._update_callback() @property def brightness(self): diff --git a/homeassistant/components/tellduslive/switch.py b/homeassistant/components/tellduslive/switch.py index 63d1512698c..bb0164b10bb 100644 --- a/homeassistant/components/tellduslive/switch.py +++ b/homeassistant/components/tellduslive/switch.py @@ -44,7 +44,9 @@ class TelldusLiveSwitch(TelldusLiveEntity, ToggleEntity): def turn_on(self, **kwargs): """Turn the switch on.""" self.device.turn_on() + self._update_callback() def turn_off(self, **kwargs): """Turn the switch off.""" self.device.turn_off() + self._update_callback() From 87b5faa2447ddc6add95b021baf300305efa1a16 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Mar 2019 16:40:29 +0100 Subject: [PATCH 252/253] Upgrade toonapilib to 3.2.1 (#21706) --- homeassistant/components/toon/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 12cae9ac801..0ca0a414fa5 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -16,7 +16,7 @@ from .const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, DATA_TOON_CLIENT, DATA_TOON_CONFIG, DOMAIN) -REQUIREMENTS = ['toonapilib==3.1.0'] +REQUIREMENTS = ['toonapilib==3.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 088800d3c4d..72185c594cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1689,7 +1689,7 @@ tikteck==0.4 todoist-python==7.0.17 # homeassistant.components.toon -toonapilib==3.1.0 +toonapilib==3.2.1 # homeassistant.components.alarm_control_panel.totalconnect total_connect_client==0.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 154d6442d25..33a88ac4391 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -295,7 +295,7 @@ srpenergy==1.0.5 statsd==3.2.1 # homeassistant.components.toon -toonapilib==3.1.0 +toonapilib==3.2.1 # homeassistant.components.camera.uvc uvcclient==0.11.0 From 21de636e5b78531c09b0672bf667e93374674a60 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 6 Mar 2019 10:07:31 -0800 Subject: [PATCH 253/253] Bumped version to 0.89.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 31f7d11734c..5b943ddb3cf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 89 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)